├── .github └── workflows │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── README_CN.md └── src ├── editor ├── components │ ├── core │ │ ├── controller.rs │ │ ├── history.rs │ │ └── mod.rs │ ├── file_opener.rs │ ├── file_saver.rs │ ├── finder.rs │ ├── helper.rs │ ├── mod.rs │ ├── positioner.rs │ └── replacer.rs ├── core │ ├── color │ │ ├── accent.rs │ │ └── mod.rs │ ├── dashboard.rs │ ├── event.rs │ ├── history.rs │ ├── init.rs │ ├── line.rs │ ├── mod.rs │ └── state.rs ├── cursor_pos.rs ├── direction.rs ├── mod.rs └── text_area.rs ├── main.rs └── utils ├── cursor.rs ├── logger.rs ├── loop_traverser.rs ├── mod.rs ├── number_bit_count.rs └── terminal.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [created] 4 | 5 | jobs: 6 | release: 7 | name: release ${{ matrix.target }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | include: 13 | - target: x86_64-pc-windows-gnu 14 | archive: zip 15 | - target: x86_64-unknown-linux-musl 16 | archive: tar.gz tar.xz tar.zst 17 | - target: x86_64-apple-darwin 18 | archive: zip 19 | steps: 20 | - uses: actions/checkout@master 21 | - name: Compile and release 22 | uses: rust-build/rust-build.action@v1.4.3 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | TOOLCHAIN_VERSION: stable 27 | RUSTTARGET: ${{ matrix.target }} 28 | ARCHIVE_TYPES: ${{ matrix.archive }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /.VSCodeCounter 3 | log.* 4 | temp.* 5 | -------------------------------------------------------------------------------- /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 = "anstream" 7 | version = "0.6.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" 10 | dependencies = [ 11 | "anstyle", 12 | "anstyle-parse", 13 | "anstyle-query", 14 | "anstyle-wincon", 15 | "colorchoice", 16 | "utf8parse", 17 | ] 18 | 19 | [[package]] 20 | name = "anstyle" 21 | version = "1.0.4" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" 24 | 25 | [[package]] 26 | name = "anstyle-parse" 27 | version = "0.2.2" 28 | source = "registry+https://github.com/rust-lang/crates.io-index" 29 | checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" 30 | dependencies = [ 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle-query" 36 | version = "1.0.0" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 39 | dependencies = [ 40 | "windows-sys", 41 | ] 42 | 43 | [[package]] 44 | name = "anstyle-wincon" 45 | version = "3.0.1" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" 48 | dependencies = [ 49 | "anstyle", 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "autocfg" 55 | version = "1.1.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 58 | 59 | [[package]] 60 | name = "bitflags" 61 | version = "1.3.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 64 | 65 | [[package]] 66 | name = "bitflags" 67 | version = "2.4.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" 70 | 71 | [[package]] 72 | name = "cfg-if" 73 | version = "1.0.0" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 76 | 77 | [[package]] 78 | name = "clap" 79 | version = "4.4.6" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" 82 | dependencies = [ 83 | "clap_builder", 84 | "clap_derive", 85 | ] 86 | 87 | [[package]] 88 | name = "clap_builder" 89 | version = "4.4.6" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" 92 | dependencies = [ 93 | "anstream", 94 | "anstyle", 95 | "clap_lex", 96 | "strsim", 97 | ] 98 | 99 | [[package]] 100 | name = "clap_derive" 101 | version = "4.4.2" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" 104 | dependencies = [ 105 | "heck", 106 | "proc-macro2", 107 | "quote", 108 | "syn", 109 | ] 110 | 111 | [[package]] 112 | name = "clap_lex" 113 | version = "0.5.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 116 | 117 | [[package]] 118 | name = "colorchoice" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 122 | 123 | [[package]] 124 | name = "crossterm" 125 | version = "0.27.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" 128 | dependencies = [ 129 | "bitflags 2.4.0", 130 | "crossterm_winapi", 131 | "libc", 132 | "mio", 133 | "parking_lot", 134 | "signal-hook", 135 | "signal-hook-mio", 136 | "winapi", 137 | ] 138 | 139 | [[package]] 140 | name = "crossterm_winapi" 141 | version = "0.9.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 144 | dependencies = [ 145 | "winapi", 146 | ] 147 | 148 | [[package]] 149 | name = "heck" 150 | version = "0.4.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 153 | 154 | [[package]] 155 | name = "libc" 156 | version = "0.2.147" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 159 | 160 | [[package]] 161 | name = "lock_api" 162 | version = "0.4.10" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 165 | dependencies = [ 166 | "autocfg", 167 | "scopeguard", 168 | ] 169 | 170 | [[package]] 171 | name = "log" 172 | version = "0.4.19" 173 | source = "registry+https://github.com/rust-lang/crates.io-index" 174 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 175 | 176 | [[package]] 177 | name = "mio" 178 | version = "0.8.8" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 181 | dependencies = [ 182 | "libc", 183 | "log", 184 | "wasi", 185 | "windows-sys", 186 | ] 187 | 188 | [[package]] 189 | name = "parking_lot" 190 | version = "0.12.1" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 193 | dependencies = [ 194 | "lock_api", 195 | "parking_lot_core", 196 | ] 197 | 198 | [[package]] 199 | name = "parking_lot_core" 200 | version = "0.9.8" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 203 | dependencies = [ 204 | "cfg-if", 205 | "libc", 206 | "redox_syscall", 207 | "smallvec", 208 | "windows-targets", 209 | ] 210 | 211 | [[package]] 212 | name = "proc-macro2" 213 | version = "1.0.68" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" 216 | dependencies = [ 217 | "unicode-ident", 218 | ] 219 | 220 | [[package]] 221 | name = "quote" 222 | version = "1.0.33" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 225 | dependencies = [ 226 | "proc-macro2", 227 | ] 228 | 229 | [[package]] 230 | name = "redox_syscall" 231 | version = "0.3.5" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 234 | dependencies = [ 235 | "bitflags 1.3.2", 236 | ] 237 | 238 | [[package]] 239 | name = "rusditor" 240 | version = "0.6.3" 241 | dependencies = [ 242 | "clap", 243 | "crossterm", 244 | ] 245 | 246 | [[package]] 247 | name = "scopeguard" 248 | version = "1.2.0" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 251 | 252 | [[package]] 253 | name = "signal-hook" 254 | version = "0.3.17" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 257 | dependencies = [ 258 | "libc", 259 | "signal-hook-registry", 260 | ] 261 | 262 | [[package]] 263 | name = "signal-hook-mio" 264 | version = "0.2.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 267 | dependencies = [ 268 | "libc", 269 | "mio", 270 | "signal-hook", 271 | ] 272 | 273 | [[package]] 274 | name = "signal-hook-registry" 275 | version = "1.4.1" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 278 | dependencies = [ 279 | "libc", 280 | ] 281 | 282 | [[package]] 283 | name = "smallvec" 284 | version = "1.11.0" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" 287 | 288 | [[package]] 289 | name = "strsim" 290 | version = "0.10.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 293 | 294 | [[package]] 295 | name = "syn" 296 | version = "2.0.38" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "unicode-ident", 303 | ] 304 | 305 | [[package]] 306 | name = "unicode-ident" 307 | version = "1.0.12" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 310 | 311 | [[package]] 312 | name = "utf8parse" 313 | version = "0.2.1" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 316 | 317 | [[package]] 318 | name = "wasi" 319 | version = "0.11.0+wasi-snapshot-preview1" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 322 | 323 | [[package]] 324 | name = "winapi" 325 | version = "0.3.9" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 328 | dependencies = [ 329 | "winapi-i686-pc-windows-gnu", 330 | "winapi-x86_64-pc-windows-gnu", 331 | ] 332 | 333 | [[package]] 334 | name = "winapi-i686-pc-windows-gnu" 335 | version = "0.4.0" 336 | source = "registry+https://github.com/rust-lang/crates.io-index" 337 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 338 | 339 | [[package]] 340 | name = "winapi-x86_64-pc-windows-gnu" 341 | version = "0.4.0" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 344 | 345 | [[package]] 346 | name = "windows-sys" 347 | version = "0.48.0" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 350 | dependencies = [ 351 | "windows-targets", 352 | ] 353 | 354 | [[package]] 355 | name = "windows-targets" 356 | version = "0.48.1" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 359 | dependencies = [ 360 | "windows_aarch64_gnullvm", 361 | "windows_aarch64_msvc", 362 | "windows_i686_gnu", 363 | "windows_i686_msvc", 364 | "windows_x86_64_gnu", 365 | "windows_x86_64_gnullvm", 366 | "windows_x86_64_msvc", 367 | ] 368 | 369 | [[package]] 370 | name = "windows_aarch64_gnullvm" 371 | version = "0.48.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 374 | 375 | [[package]] 376 | name = "windows_aarch64_msvc" 377 | version = "0.48.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 380 | 381 | [[package]] 382 | name = "windows_i686_gnu" 383 | version = "0.48.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 386 | 387 | [[package]] 388 | name = "windows_i686_msvc" 389 | version = "0.48.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 392 | 393 | [[package]] 394 | name = "windows_x86_64_gnu" 395 | version = "0.48.0" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 398 | 399 | [[package]] 400 | name = "windows_x86_64_gnullvm" 401 | version = "0.48.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 404 | 405 | [[package]] 406 | name = "windows_x86_64_msvc" 407 | version = "0.48.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 410 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusditor" 3 | version = "0.6.3" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.4.6", features = ["derive"] } 10 | crossterm = "0.27.0" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rusditor 2 | 3 | English | [简体中文](./README_CN.md) 4 | 5 | A simple text editor runs on terminals. 6 | 7 | ## Shortcuts 8 | 9 | | Key | Function | 10 | | --- | --- | 11 | | Ctrl + s | Open / Close file saving component | 12 | | Enter (When file-saver opened) | Write file | 13 | | Ctrl + o | Open / Close file opening component | 14 | | Enter (When file-opener opened) | Open file | 15 | | Ctrl + g | Open / Close positioner component | 16 | | Enter (When positioner opened) | Jump to target position | 17 | | Ctrl + f | Open / Close finder component | 18 | | Enter (When finder opened) | Jump to next matches target | 19 | | Shift + Enter (When finder opened) | Jump to previous matches target | 20 | | Ctrl + r | Open / Close text replacer | 21 | | Enter (When text replacer opened) | Toggle text replacer mode to replacing mode (searching mode default) | 22 | | Ctrl + n (When text replacer opened and is replacing) | Jump to next matching text | 23 | | Ctrl + s (When text replacer opened and is replacing) | Replace single matching text | 24 | | Ctrl + a (When text replacer opened and is replacing) | Replace all matching text | 25 | | Ctrl + z | Undo | 26 | | Ctrl + y | Redo | 27 | | Esc | Restore to normal mode (not in normal mode) / Exit program (in normal mode) | 28 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # Rusditor 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | 一个运行在终端上的简易文本编辑器 6 | 7 | ## 快捷键 8 | 9 | | 按键 | 功能 | 10 | | --- | --- | 11 | | Ctrl + s | 开启 / 关闭 文件保存组件 | 12 | | Enter (当文件保存组件启用时) | 写入文件 | 13 | | Ctrl + o | 开启 / 关闭 文件打开组件 | 14 | | Enter (当文件打开组件启用时) | 打开文件 | 15 | | Ctrl + g | 开启 / 关闭 定位组件 | 16 | | Enter (当定位组件启用时) | 跳转到指定位置 | 17 | | Ctrl + f | 开启 / 关闭 查询组件 | 18 | | Enter (当查询组件启用时) | 跳转到下一个匹配的文本位置 | 19 | | Shift + Enter (当查询组件启用时) | 跳转到上一个匹配的文本位置 | 20 | | Ctrl + r | 开启 / 关闭 文本替换组件 | 21 | | Enter (当文本替换组件启用时) | 切换文本替换组件到替换模式 (默认为查找模式) | 22 | | Ctrl + n (当文本替换组件启用并且处于替换模式时) | 跳转到下一个匹配的文本位置 | 23 | | Ctrl + s (当文本替换组件启用并且处于替换模式时) | 替换单个匹配的文本 | 24 | | Ctrl + a (当文本替换组件启用并且处于替换模式时) | 替换全部匹配的文本 | 25 | | Ctrl + z | 撤销 | 26 | | Ctrl + y | 恢复 | 27 | | Esc | 恢复编辑模式 (当处于非编辑模式) / 退出程序 (当处于编辑模式) | 28 | -------------------------------------------------------------------------------- /src/editor/components/core/controller.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::{event::KeyCode, style::Stylize}; 4 | 5 | use crate::{ 6 | editor::{direction::Direction, text_area::TextArea}, 7 | utils::{Cursor, Terminal}, 8 | }; 9 | 10 | pub struct LineComponentController { 11 | pub prompt: &'static str, 12 | pub button: &'static str, 13 | pub text_area: TextArea, 14 | 15 | // verticle position to show, 16 | // if less than zero, equal to 17 | // Terminal::height() - position - 1 18 | pub position: isize, 19 | 20 | pub editable: bool, 21 | } 22 | 23 | impl LineComponentController { 24 | pub fn open(&mut self) -> io::Result<()> { 25 | let render_pos = if self.position >= 0 { 26 | self.position as usize 27 | } else { 28 | (Terminal::height() as isize + self.position - 1) as usize 29 | }; 30 | 31 | Cursor::move_to_row(render_pos)?; 32 | Cursor::move_to_col(0)?; 33 | print!("{}", self.prompt.bold().black().on_white()); 34 | 35 | Cursor::move_to_col(Terminal::width() - self.button.len())?; 36 | print!("{}", self.button.bold().black().on_white()); 37 | 38 | self.text_area.move_cursor_to_end(false)?; 39 | self.text_area.render()?; 40 | return Ok(()); 41 | } 42 | 43 | pub fn edit(&mut self, key: KeyCode) -> io::Result<()> { 44 | if !self.editable { 45 | return Ok(()); 46 | } 47 | 48 | let text_area = &mut self.text_area; 49 | match key { 50 | KeyCode::Backspace => { 51 | text_area.delete_char(true)?; 52 | } 53 | 54 | KeyCode::Left => text_area.move_cursor_horizontal(Direction::Left, true)?, 55 | KeyCode::Right => text_area.move_cursor_horizontal(Direction::Right, true)?, 56 | KeyCode::Char(ch) => text_area.insert_char(ch, true)?, 57 | _ => unreachable!(), 58 | } 59 | return Ok(()); 60 | } 61 | } 62 | 63 | // --- --- --- --- --- --- 64 | 65 | pub struct ScreenComponentController { 66 | pub content: String, 67 | } 68 | 69 | impl ScreenComponentController { 70 | pub fn render(&self) -> io::Result<()> { 71 | Cursor::move_to_col(1)?; 72 | Cursor::move_to_row(0)?; 73 | 74 | let term_width = Terminal::width(); 75 | let mut slice = &self.content[..]; 76 | while !slice.is_empty() { 77 | if slice.len() > term_width { 78 | print!("{}", &slice[0..term_width]); 79 | slice = &slice[term_width..]; 80 | Cursor::down(1)?; 81 | } else { 82 | print!("{}", slice); 83 | break; 84 | } 85 | } 86 | return Ok(()); 87 | } 88 | } -------------------------------------------------------------------------------- /src/editor/components/core/history.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::LoopTraverser; 2 | 3 | pub struct ComponentHistory { 4 | list: LoopTraverser, 5 | cached_content: String, 6 | 7 | // is using history content 8 | pub use_history: bool, 9 | } 10 | 11 | impl ComponentHistory { 12 | pub const HISTORY_PLACEHOLDER: &'static str = "Up & Down for history"; 13 | 14 | pub fn new() -> Self { 15 | Self { 16 | list: LoopTraverser::new(false), 17 | cached_content: String::new(), 18 | use_history: false, 19 | } 20 | } 21 | 22 | pub fn next(&mut self) -> Option<&String> { 23 | let previous = self.list.previous(); 24 | if previous.is_none() { 25 | self.use_history = false; 26 | return Some(&self.cached_content); 27 | } else { 28 | return previous; 29 | } 30 | } 31 | pub fn previous(&mut self) -> Option<&String> { 32 | self.use_history = true; 33 | self.list.next() 34 | } 35 | 36 | #[inline] 37 | pub fn set_cached(&mut self, content: String) { 38 | self.cached_content = content.to_owned(); 39 | } 40 | 41 | #[inline] 42 | pub fn last(&self) -> Option<&String> { 43 | self.list.first() 44 | } 45 | 46 | #[inline] 47 | pub fn append(&mut self, element: String) { 48 | self.list.push_front(element); 49 | } 50 | #[inline] 51 | pub fn reset_index(&mut self) { 52 | self.cached_content.clear(); 53 | self.list.reset_index(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/editor/components/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod controller; 2 | mod history; 3 | 4 | pub use controller::{LineComponentController, ScreenComponentController}; 5 | pub use history::ComponentHistory; 6 | 7 | use std::io; 8 | 9 | use crossterm::event::KeyEvent; 10 | 11 | use crate::editor::text_area::TextArea; 12 | 13 | pub trait LineComponent { 14 | const PROMPT: &'static str; 15 | const BUTTON: &'static str; 16 | const POSITION: isize; 17 | const EDITABLE: bool; 18 | 19 | fn init_controller() -> LineComponentController { 20 | LineComponentController { 21 | prompt: Self::PROMPT, 22 | button: Self::BUTTON, 23 | text_area: TextArea::new(Self::PROMPT.len(), Self::BUTTON.len()), 24 | 25 | position: Self::POSITION, 26 | editable: Self::EDITABLE, 27 | } 28 | } 29 | fn open(&mut self) -> io::Result<()>; 30 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()>; 31 | } 32 | -------------------------------------------------------------------------------- /src/editor/components/file_opener.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | use crate::editor::text_area::TextArea; 6 | 7 | use super::core::{LineComponent, LineComponentController}; 8 | 9 | pub struct FileOpener { 10 | comp: LineComponentController, 11 | } 12 | 13 | impl FileOpener { 14 | pub fn new() -> Self { 15 | Self { 16 | comp: Self::init_controller(), 17 | } 18 | } 19 | 20 | #[inline] 21 | pub fn get_file_path(&mut self) -> &str { 22 | self.comp.text_area.content() 23 | } 24 | 25 | #[inline] 26 | pub fn is_open_file_callback_key(key: KeyEvent) -> bool { 27 | key.modifiers == KeyModifiers::NONE && key.code == KeyCode::Enter 28 | } 29 | 30 | #[inline] 31 | pub fn set_path(&mut self, path: &str) { 32 | self.comp.text_area.set_content(path); 33 | } 34 | } 35 | 36 | impl LineComponent for FileOpener { 37 | const PROMPT: &'static str = "Path: "; 38 | const BUTTON: &'static str = "[Enter]"; 39 | const POSITION: isize = 1; 40 | const EDITABLE: bool = true; 41 | 42 | #[inline] 43 | fn open(&mut self) -> io::Result<()> { 44 | self.comp.open() 45 | } 46 | 47 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 48 | if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT { 49 | if TextArea::is_editing_key(key.code) { 50 | self.comp.edit(key.code)?; 51 | } 52 | } 53 | return Ok(()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/editor/components/file_saver.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io, 4 | path::Path, 5 | }; 6 | 7 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 8 | 9 | use crate::editor::text_area::TextArea; 10 | 11 | use super::core::{LineComponent, LineComponentController}; 12 | 13 | pub struct FileSaver { 14 | editor_content: String, 15 | comp: LineComponentController, 16 | } 17 | 18 | impl FileSaver { 19 | const DEFAULT_FILE_NAME: &'static str = "temp.txt"; 20 | 21 | pub fn new() -> Self { 22 | let mut controller = Self::init_controller(); 23 | controller.text_area.set_content(Self::DEFAULT_FILE_NAME); 24 | return Self { 25 | editor_content: String::new(), 26 | comp: controller, 27 | }; 28 | } 29 | 30 | fn save(&self) -> io::Result<()> { 31 | let target_path_str = self.comp.text_area.content(); 32 | let target_path = Path::new(target_path_str); 33 | 34 | if !target_path.exists() { 35 | File::create(target_path)?; 36 | } 37 | let bytes_to_write = self.editor_content.as_bytes(); 38 | fs::write(target_path_str, bytes_to_write)?; 39 | return Ok(()); 40 | } 41 | 42 | #[inline] 43 | pub fn is_save_callback_key(key: KeyEvent) -> bool { 44 | key.modifiers == KeyModifiers::NONE && key.code == KeyCode::Enter 45 | } 46 | 47 | #[inline] 48 | pub fn set_content(&mut self, content: String) { 49 | self.editor_content = content; 50 | } 51 | 52 | #[inline] 53 | pub fn set_path(&mut self, path: &str) { 54 | self.comp.text_area.set_content(path); 55 | } 56 | } 57 | 58 | impl LineComponent for FileSaver { 59 | const PROMPT: &'static str = "Path: "; 60 | const BUTTON: &'static str = "[Enter]"; 61 | const POSITION: isize = 1; 62 | const EDITABLE: bool = true; 63 | 64 | #[inline] 65 | fn open(&mut self) -> io::Result<()> { 66 | self.comp.open() 67 | } 68 | 69 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 70 | if key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT { 71 | match key.code { 72 | KeyCode::Enter => self.save()?, 73 | k if TextArea::is_editing_key(k) => self.comp.edit(k)?, 74 | _ => {} 75 | } 76 | } 77 | return Ok(()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/editor/components/finder.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | use crate::{ 6 | editor::{cursor_pos::EditorCursorPos, text_area::TextArea}, 7 | utils::LoopTraverser, 8 | }; 9 | 10 | use super::{ 11 | core::{LineComponentController, ComponentHistory}, 12 | LineComponent, 13 | }; 14 | 15 | pub struct Finder { 16 | match_list: LoopTraverser, 17 | 18 | history: ComponentHistory, 19 | comp: LineComponentController, 20 | } 21 | 22 | impl Finder { 23 | pub fn new() -> Self { 24 | let mut controller = Self::init_controller(); 25 | controller 26 | .text_area 27 | .set_placeholder(ComponentHistory::HISTORY_PLACEHOLDER); 28 | 29 | return Self { 30 | match_list: LoopTraverser::new(true), 31 | 32 | history: ComponentHistory::new(), 33 | comp: controller, 34 | }; 35 | } 36 | 37 | #[inline] 38 | pub fn set_matches(&mut self, pos_list: Vec) { 39 | self.match_list.set_content(pos_list); 40 | } 41 | 42 | #[inline] 43 | pub fn next(&mut self) -> Option<&EditorCursorPos> { 44 | self.match_list.next() 45 | } 46 | #[inline] 47 | pub fn previous(&mut self) -> Option<&EditorCursorPos> { 48 | self.match_list.previous() 49 | } 50 | 51 | #[inline] 52 | pub fn content(&self) -> &str { 53 | self.comp.text_area.content() 54 | } 55 | #[inline] 56 | pub fn is_empty(&self) -> bool { 57 | self.match_list.is_empty() 58 | } 59 | #[inline] 60 | pub fn clear(&mut self) { 61 | self.comp.text_area.clear(); 62 | self.match_list.clear(); 63 | } 64 | 65 | // --- --- --- --- --- --- 66 | 67 | #[inline] 68 | pub fn is_finding_key(key: KeyEvent) -> bool { 69 | key.modifiers == KeyModifiers::NONE && key.code == KeyCode::Enter 70 | } 71 | #[inline] 72 | pub fn is_reverse_finding_key(key: KeyEvent) -> bool { 73 | key.modifiers == KeyModifiers::SHIFT && key.code == KeyCode::Enter 74 | } 75 | } 76 | 77 | impl LineComponent for Finder { 78 | const PROMPT: &'static str = "Find: "; 79 | const BUTTON: &'static str = "[(Shift) Enter]"; 80 | const POSITION: isize = -1; 81 | const EDITABLE: bool = true; 82 | 83 | fn open(&mut self) -> io::Result<()> { 84 | self.comp.open() 85 | } 86 | 87 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 88 | if !(key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT) { 89 | return Ok(()); 90 | } 91 | 92 | match key.code { 93 | KeyCode::Up | KeyCode::Down => { 94 | let history_content = match key.code { 95 | KeyCode::Up => { 96 | if !self.history.use_history { 97 | let current_content = self.content().to_owned(); 98 | self.history.set_cached(current_content); 99 | } 100 | self.history.previous() 101 | } 102 | KeyCode::Down => self.history.next(), 103 | _ => unreachable!(), 104 | }; 105 | if let Some(str) = history_content { 106 | let text_area = &mut self.comp.text_area; 107 | text_area.set_content(str); 108 | text_area.move_cursor_to_end(false)?; 109 | text_area.render()?; 110 | } 111 | } 112 | KeyCode::Enter => { 113 | let current_target = self.content(); 114 | if let Some(last_appended) = self.history.last() { 115 | // avoid repetitive history content 116 | if current_target == last_appended { 117 | return Ok(()); 118 | } 119 | } 120 | self.history.append(current_target.to_owned()); 121 | } 122 | k if TextArea::is_editing_key(k) => { 123 | self.history.reset_index(); 124 | self.comp.edit(k)?; 125 | } 126 | _ => {} 127 | } 128 | return Ok(()); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/editor/components/helper.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::KeyEvent; 4 | 5 | use super::core::ScreenComponentController; 6 | 7 | pub struct Helper { 8 | comp: ScreenComponentController, 9 | } 10 | 11 | impl Helper { 12 | pub fn new() -> Self { 13 | Helper { 14 | comp: ScreenComponentController { 15 | content: String::from("Ctrl + s | Open / Close file saving component"), 16 | } 17 | } 18 | } 19 | 20 | #[inline] 21 | pub fn open(&self) -> io::Result<()> { 22 | self.comp.render() 23 | } 24 | 25 | pub fn key_resolve(&self, key: KeyEvent) -> io::Result<()> { 26 | return Ok(()); 27 | } 28 | } -------------------------------------------------------------------------------- /src/editor/components/mod.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod file_saver; 3 | mod file_opener; 4 | mod finder; 5 | mod positioner; 6 | mod replacer; 7 | mod helper; 8 | 9 | pub use self::core::LineComponent; 10 | pub use file_saver::FileSaver; 11 | pub use file_opener::FileOpener; 12 | pub use finder::Finder; 13 | pub use positioner::Positioner; 14 | pub use replacer::Replacer; 15 | pub use helper::Helper; 16 | 17 | use std::io; 18 | 19 | use crossterm::event::KeyEvent; 20 | 21 | use super::core::EditorState; 22 | 23 | pub struct EditorComponentManager { 24 | pub use_line_component: bool, 25 | pub use_screen_component: bool, 26 | 27 | // line components 28 | pub file_saver: FileSaver, 29 | pub file_opener: FileOpener, 30 | pub positioner: Positioner, 31 | pub finder: Finder, 32 | pub replacer: Replacer, 33 | 34 | // screen components 35 | pub helper: Helper, 36 | } 37 | 38 | impl EditorComponentManager { 39 | pub fn new() -> Self { 40 | Self { 41 | use_line_component: false, 42 | use_screen_component: false, 43 | 44 | file_saver: FileSaver::new(), 45 | file_opener: FileOpener::new(), 46 | positioner: Positioner::new(), 47 | finder: Finder::new(), 48 | replacer: Replacer::new(), 49 | 50 | helper: Helper::new(), 51 | } 52 | } 53 | 54 | pub fn resolve(&mut self, current_state: EditorState, key: KeyEvent) -> io::Result<()> { 55 | match current_state { 56 | EditorState::Saving => self.file_saver.key_resolve(key)?, 57 | EditorState::Opening => self.file_opener.key_resolve(key)?, 58 | EditorState::Positioning => self.positioner.key_resolve(key)?, 59 | EditorState::Finding => self.finder.key_resolve(key)?, 60 | EditorState::Replacing => self.replacer.key_resolve(key)?, 61 | 62 | EditorState::ReadingHelpMsg => self.helper.key_resolve(key)?, 63 | _ => unreachable!(), 64 | } 65 | return Ok(()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/editor/components/positioner.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | use crate::editor::{cursor_pos::EditorCursorPos, text_area::TextArea}; 6 | 7 | use super::{core::LineComponentController, LineComponent}; 8 | 9 | pub struct Positioner { 10 | target: EditorCursorPos, 11 | comp: LineComponentController, 12 | } 13 | 14 | impl Positioner { 15 | pub fn new() -> Self { 16 | let initial_cursor_pos = EditorCursorPos { row: 1, col: 1 }; 17 | let mut controller = Self::init_controller(); 18 | controller 19 | .text_area 20 | .set_placeholder(&initial_cursor_pos.short_display()); 21 | return Self { 22 | target: initial_cursor_pos, 23 | comp: controller, 24 | }; 25 | } 26 | 27 | #[inline] 28 | pub fn set_cursor_pos(&mut self, pos: EditorCursorPos) { 29 | let pos_str = pos.short_display(); 30 | self.comp.text_area.set_placeholder(&pos_str); 31 | self.target = pos; 32 | } 33 | 34 | #[inline] 35 | pub fn is_positioning_key(key: KeyEvent) -> bool { 36 | key.modifiers == KeyModifiers::NONE && key.code == KeyCode::Enter 37 | } 38 | 39 | #[inline] 40 | pub fn get_target(&self) -> EditorCursorPos { 41 | self.target 42 | } 43 | } 44 | 45 | impl LineComponent for Positioner { 46 | const PROMPT: &'static str = "Jump to: "; 47 | const BUTTON: &'static str = "[Enter]"; 48 | const POSITION: isize = -1; 49 | const EDITABLE: bool = true; 50 | 51 | #[inline] 52 | fn open(&mut self) -> io::Result<()> { 53 | self.comp.open() 54 | } 55 | 56 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 57 | if !(key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT) { 58 | return Ok(()); 59 | } 60 | 61 | match key.code { 62 | KeyCode::Enter => { 63 | let target_pos_str = self.comp.text_area.content(); 64 | let parsed_pos = EditorCursorPos::parse(target_pos_str); 65 | self.comp.text_area.clear(); 66 | 67 | if let Some(pos) = parsed_pos { 68 | self.target = pos 69 | } 70 | } 71 | k if TextArea::is_editing_key(k) => self.comp.edit(k)?, 72 | _ => {} 73 | } 74 | return Ok(()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/editor/components/replacer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; 4 | 5 | use crate::{ 6 | editor::{cursor_pos::EditorCursorPos, text_area::TextArea}, 7 | utils::LoopTraverser, 8 | }; 9 | 10 | use super::{ 11 | core::{LineComponentController, ComponentHistory}, 12 | LineComponent, 13 | }; 14 | 15 | #[derive(PartialEq)] 16 | enum ReplacerState { 17 | Searching, 18 | Replacing, 19 | } 20 | 21 | pub struct Replacer { 22 | state: ReplacerState, 23 | match_list: LoopTraverser, 24 | 25 | search_history: ComponentHistory, 26 | replace_history: ComponentHistory, 27 | 28 | searcher: LineComponentController, 29 | replacer: LineComponentController, 30 | } 31 | 32 | impl Replacer { 33 | const REPLACE_PROMPT: &'static str = "Replace: "; 34 | const REPLACE_BUTTON: &'static str = "[Ctrl + S / N / A]"; 35 | 36 | pub fn new() -> Self { 37 | let mut searcher_controller = Self::init_controller(); 38 | let mut replacer_controller = LineComponentController { 39 | prompt: Self::REPLACE_PROMPT, 40 | button: Self::REPLACE_BUTTON, 41 | text_area: TextArea::new(Self::REPLACE_PROMPT.len(), Self::REPLACE_BUTTON.len()), 42 | position: -1, 43 | editable: true, 44 | }; 45 | searcher_controller 46 | .text_area 47 | .set_placeholder(ComponentHistory::HISTORY_PLACEHOLDER); 48 | replacer_controller 49 | .text_area 50 | .set_placeholder(ComponentHistory::HISTORY_PLACEHOLDER); 51 | 52 | return Self { 53 | state: ReplacerState::Searching, 54 | match_list: LoopTraverser::new(false), 55 | 56 | search_history: ComponentHistory::new(), 57 | replace_history: ComponentHistory::new(), 58 | 59 | searcher: searcher_controller, 60 | replacer: replacer_controller, 61 | }; 62 | } 63 | 64 | #[inline] 65 | pub fn first(&self) -> Option<&EditorCursorPos> { 66 | self.match_list.first() 67 | } 68 | 69 | #[inline] 70 | pub fn current(&self) -> &EditorCursorPos { 71 | self.match_list.current() 72 | } 73 | 74 | #[inline] 75 | pub fn next(&mut self) -> Option<&EditorCursorPos> { 76 | self.match_list.next() 77 | } 78 | 79 | #[inline] 80 | pub fn search_text(&self) -> &str { 81 | self.searcher.text_area.content() 82 | } 83 | #[inline] 84 | pub fn replace_text(&self) -> &str { 85 | self.replacer.text_area.content() 86 | } 87 | 88 | // when pressed `search_key` (Enter) and exists search result, 89 | // this handler will be called. 90 | pub fn search_handler(&mut self, pos_list: Vec) -> io::Result<()> { 91 | self.search_history.append(self.search_text().to_owned()); 92 | self.match_list.set_content(pos_list); 93 | self.replacer.open()?; 94 | self.state = ReplacerState::Replacing; 95 | return Ok(()); 96 | } 97 | 98 | // when pressed `replace_one_key` or `replace_all_key`, 99 | // this handler will be called. 100 | pub fn replace_handler(&mut self) { 101 | let current_content = self.replace_text(); 102 | if let Some(last_content) = self.replace_history.last() { 103 | // avoid repetitive content 104 | if current_content == last_content { 105 | return; 106 | } 107 | } 108 | self.replace_history.append(current_content.to_owned()); 109 | } 110 | 111 | pub fn reset(&mut self) { 112 | self.state = ReplacerState::Searching; 113 | self.searcher.text_area.clear(); 114 | self.replacer.text_area.clear(); 115 | self.match_list.clear(); 116 | } 117 | 118 | // --- --- --- --- --- --- 119 | 120 | #[inline] 121 | pub fn is_search_key(&self, key: KeyEvent) -> bool { 122 | self.state == ReplacerState::Searching 123 | && key.modifiers == KeyModifiers::NONE 124 | && key.code == KeyCode::Enter 125 | } 126 | #[inline] 127 | pub fn is_next_key(&self, key: KeyEvent) -> bool { 128 | self.state == ReplacerState::Replacing 129 | && key.modifiers == KeyModifiers::CONTROL 130 | && key.code == KeyCode::Char('n') 131 | } 132 | #[inline] 133 | pub fn is_replace_one_key(&self, key: KeyEvent) -> bool { 134 | self.state == ReplacerState::Replacing 135 | && key.modifiers == KeyModifiers::CONTROL 136 | && key.code == KeyCode::Char('s') 137 | } 138 | #[inline] 139 | pub fn is_replace_all_key(&self, key: KeyEvent) -> bool { 140 | self.state == ReplacerState::Replacing 141 | && key.modifiers == KeyModifiers::CONTROL 142 | && key.code == KeyCode::Char('a') 143 | } 144 | } 145 | 146 | impl LineComponent for Replacer { 147 | const BUTTON: &'static str = "[Enter]"; 148 | const PROMPT: &'static str = "Search: "; 149 | const EDITABLE: bool = true; 150 | const POSITION: isize = -1; 151 | 152 | fn open(&mut self) -> io::Result<()> { 153 | match self.state { 154 | ReplacerState::Searching => &mut self.searcher, 155 | ReplacerState::Replacing => &mut self.replacer, 156 | } 157 | .open() 158 | } 159 | fn key_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 160 | if !(key.modifiers == KeyModifiers::NONE || key.modifiers == KeyModifiers::SHIFT) { 161 | return Ok(()); 162 | } 163 | 164 | match key.code { 165 | KeyCode::Up | KeyCode::Down => { 166 | let (current_content, target_history) = match self.state { 167 | ReplacerState::Searching => { 168 | (self.search_text().to_owned(), &mut self.search_history) 169 | } 170 | ReplacerState::Replacing => { 171 | (self.replace_text().to_owned(), &mut self.replace_history) 172 | } 173 | }; 174 | let history_content = match key.code { 175 | KeyCode::Up => { 176 | if !target_history.use_history { 177 | target_history.set_cached(current_content); 178 | } 179 | target_history.previous() 180 | } 181 | KeyCode::Down => target_history.next(), 182 | _ => unreachable!(), 183 | }; 184 | if let Some(str) = history_content { 185 | let text_area = match self.state { 186 | ReplacerState::Searching => &mut self.searcher.text_area, 187 | ReplacerState::Replacing => &mut self.replacer.text_area, 188 | }; 189 | text_area.set_content(str); 190 | text_area.move_cursor_to_end(false)?; 191 | text_area.render()?; 192 | } 193 | } 194 | k if TextArea::is_editing_key(k) => match self.state { 195 | ReplacerState::Searching => &mut self.searcher, 196 | ReplacerState::Replacing => &mut self.replacer, 197 | } 198 | .edit(key.code)?, 199 | _ => {} 200 | } 201 | return Ok(()); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/editor/core/color/accent.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | pub enum AccentColor { 3 | Red, 4 | Blue, 5 | DarkRed, 6 | DarkBlue, 7 | DarkGrey, 8 | DarkCyan, 9 | DarkYellow, 10 | DarkMagenta, 11 | } 12 | impl From<&str> for AccentColor { 13 | fn from(value: &str) -> Self { 14 | match value { 15 | "red" => Self::Red, 16 | "blue" => Self::Blue, 17 | "dark_red" => Self::DarkRed, 18 | "dark_blue" => Self::DarkBlue, 19 | "dark_grey" => Self::DarkGrey, 20 | "dark_cyan" => Self::DarkCyan, 21 | "dark_yellow" => Self::DarkYellow, 22 | "dark_magenta" => Self::DarkMagenta, 23 | _ => Self::Red, 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/editor/core/color/mod.rs: -------------------------------------------------------------------------------- 1 | mod accent; 2 | 3 | use std::fmt::Display; 4 | 5 | use crossterm::style::{StyledContent, Stylize}; 6 | 7 | use accent::AccentColor; 8 | 9 | pub struct EditorColor; 10 | static mut ACCENT_COLOR: AccentColor = AccentColor::Red; 11 | 12 | impl EditorColor { 13 | #[inline] 14 | pub fn set_accent_color(color: &str) { 15 | unsafe { ACCENT_COLOR = AccentColor::from(color) } 16 | } 17 | 18 | pub fn highlight_style(content: D) -> StyledContent 19 | where 20 | D: Display + Stylize>, 21 | { 22 | let mut styled = content.white(); 23 | styled = match unsafe { ACCENT_COLOR } { 24 | AccentColor::Red => styled.on_red(), 25 | AccentColor::Blue => styled.on_blue(), 26 | AccentColor::DarkRed => styled.on_dark_red(), 27 | AccentColor::DarkBlue => styled.on_dark_blue(), 28 | AccentColor::DarkGrey => styled.on_dark_grey(), 29 | AccentColor::DarkCyan => styled.on_dark_cyan(), 30 | AccentColor::DarkYellow => styled.on_dark_yellow(), 31 | AccentColor::DarkMagenta => styled.on_magenta(), 32 | }; 33 | return styled; 34 | } 35 | 36 | #[inline] 37 | pub fn line_active_style(content: D) -> StyledContent 38 | where 39 | D: Display + Stylize>, 40 | { 41 | content.bold().black().on_white() 42 | } 43 | #[inline] 44 | pub fn line_disabled_style(content: D) -> StyledContent 45 | where 46 | D: Display + Stylize>, 47 | { 48 | content.dark_grey().on_grey() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/editor/core/dashboard.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::style::Stylize; 4 | 5 | use super::EditorState; 6 | use crate::editor::core::color::EditorColor; 7 | use crate::editor::cursor_pos::{EditorCursorPos, TerminalCursorPos}; 8 | use crate::utils::{Cursor, Terminal}; 9 | 10 | pub struct EditorDashboard { 11 | cursor_pos: EditorCursorPos, 12 | state: EditorState, 13 | 14 | // this cursor position is used to temporarily 15 | // save and restore cursor. 16 | temp_cursor_pos: TerminalCursorPos, 17 | 18 | // this state is used to cache current state when 19 | // component state is set. 20 | saved_state: EditorState, 21 | } 22 | 23 | impl EditorDashboard { 24 | pub fn new() -> Self { 25 | Self { 26 | cursor_pos: EditorCursorPos { row: 1, col: 1 }, 27 | state: EditorState::Saved, 28 | 29 | temp_cursor_pos: TerminalCursorPos { row: 1, col: 1 }, 30 | saved_state: EditorState::Saved, 31 | } 32 | } 33 | 34 | pub fn render(&mut self) -> io::Result<()> { 35 | self.temp_cursor_pos.save_pos()?; 36 | 37 | // move cursor to start of the last row 38 | Cursor::move_to_row(Terminal::height() - 1)?; 39 | Cursor::move_to_col(0)?; 40 | 41 | let state_str = format!(" {} ", self.state); 42 | let cursor_pos_str = format!(" {} ", self.cursor_pos); 43 | 44 | // `2` here is space for left-margin and right-margin 45 | let remain_space = Terminal::width() - state_str.len() - cursor_pos_str.len(); 46 | let divider_str = " ".repeat(remain_space).on_white(); 47 | 48 | print!( 49 | "{}{divider_str}{}", 50 | EditorColor::highlight_style(state_str), 51 | EditorColor::highlight_style(cursor_pos_str) 52 | ); 53 | self.temp_cursor_pos.restore_pos()?; 54 | return Ok(()); 55 | } 56 | 57 | #[inline] 58 | pub fn state(&self) -> EditorState { 59 | self.state 60 | } 61 | 62 | pub fn set_state(&mut self, new_state: EditorState) -> io::Result<()> { 63 | if new_state.is_component_state() { 64 | // cache current state 65 | self.saved_state = self.state; 66 | } else { 67 | self.saved_state = new_state; 68 | } 69 | self.state = new_state; 70 | 71 | self.render()?; 72 | return Ok(()); 73 | } 74 | 75 | pub fn restore_state(&mut self) -> io::Result<()> { 76 | self.state = self.saved_state; 77 | self.render()?; 78 | return Ok(()); 79 | } 80 | 81 | pub fn set_cursor_pos(&mut self, pos: EditorCursorPos) -> io::Result<()> { 82 | (self.cursor_pos.row, self.cursor_pos.col) = (pos.row, pos.col); 83 | self.render()?; 84 | return Ok(()); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/editor/core/event.rs: -------------------------------------------------------------------------------- 1 | use crate::editor::cursor_pos::EditorCursorPos; 2 | 3 | #[derive(Debug, Clone)] 4 | pub enum EditorOperation { 5 | InsertChar(char), 6 | DeleteChar(char), 7 | InsertLine, 8 | DeleteLine, 9 | 10 | // from , to 11 | Replace(String, String), 12 | } 13 | 14 | impl EditorOperation { 15 | pub fn rev(&self) -> Self { 16 | match self { 17 | Self::InsertChar(ch) => Self::DeleteChar(*ch), 18 | Self::DeleteChar(ch) => Self::InsertChar(*ch), 19 | Self::InsertLine => Self::DeleteLine, 20 | Self::DeleteLine => Self::InsertLine, 21 | 22 | Self::Replace(from, to) => Self::Replace(to.clone(), from.clone()), 23 | } 24 | } 25 | } 26 | 27 | // --- --- --- --- --- --- 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct EditorEvent { 31 | pub op: EditorOperation, 32 | 33 | pub pos_before: EditorCursorPos, 34 | pub pos_after: EditorCursorPos, 35 | } 36 | -------------------------------------------------------------------------------- /src/editor/core/history.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use super::event::EditorEvent; 4 | 5 | #[derive(Debug)] 6 | pub struct EditorHistory { 7 | events: VecDeque, 8 | 9 | // events that is undone 10 | undo_events: Vec, 11 | // events that is redone 12 | redo_events: Vec, 13 | } 14 | 15 | impl EditorHistory { 16 | const MAX_CACHED_EVENT: usize = 255; 17 | 18 | pub fn new() -> Self { 19 | Self { 20 | events: VecDeque::::new(), 21 | undo_events: Vec::::new(), 22 | redo_events: Vec::::new(), 23 | } 24 | } 25 | 26 | #[inline] 27 | // returns the last appended event 28 | pub fn previous_event(&self) -> Option<&EditorEvent> { 29 | self.events.back() 30 | } 31 | 32 | pub fn undo(&mut self) -> Option<&EditorEvent> { 33 | let option_op = if !self.redo_events.is_empty() { 34 | self.redo_events.pop() 35 | } else { 36 | self.events.pop_back() 37 | }; 38 | 39 | match option_op { 40 | Some(op) => { 41 | self.undo_events.push(op); 42 | self.undo_events.last() 43 | } 44 | None => None, 45 | } 46 | } 47 | 48 | pub fn redo(&mut self) -> Option<&EditorEvent> { 49 | match self.undo_events.pop() { 50 | Some(op) => { 51 | self.redo_events.push(op); 52 | self.redo_events.last() 53 | } 54 | None => None, 55 | } 56 | } 57 | 58 | pub fn append(&mut self, ev: EditorEvent) { 59 | self.undo_events.clear(); 60 | self.redo_events.clear(); 61 | self.events.push_back(ev); 62 | 63 | if self.events.len() > Self::MAX_CACHED_EVENT { 64 | self.events.pop_front(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/editor/core/init.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::style::Stylize; 4 | 5 | use crate::{utils::{Cursor, Terminal}, editor::core::color::EditorColor}; 6 | 7 | pub struct EditorInit; 8 | 9 | impl EditorInit { 10 | pub fn display_title() { 11 | let term_width = Terminal::width(); 12 | let title_str = format!("Rusditor v{}", env!("CARGO_PKG_VERSION")); 13 | let esc_button_str = " [Esc] "; 14 | 15 | let elements_width = title_str.len() + esc_button_str.len(); 16 | let padding_width1 = (term_width - elements_width) / 2; 17 | let padding_width2 = term_width - padding_width1 - elements_width; 18 | let (padding_str1, padding_str2) = (" ".repeat(padding_width1), " ".repeat(padding_width2)); 19 | 20 | print!( 21 | "{}{}{}{}", 22 | padding_str1.on_white(), 23 | title_str.bold().black().on_white(), 24 | padding_str2.on_white(), 25 | EditorColor::highlight_style(esc_button_str), 26 | ); 27 | } 28 | 29 | pub fn display_border() -> io::Result<()> { 30 | // print left and right border 31 | for _ in 1..Terminal::height() { 32 | print!("{}", " ".on_white()); 33 | Cursor::down(1)?; 34 | Cursor::move_to_col(0)?; 35 | } 36 | return Ok(()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/editor/core/line.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crate::{ 4 | editor::{core::color::EditorColor, direction::Direction, text_area::TextArea}, 5 | utils::{number_bit_count, Cursor, Terminal}, 6 | }; 7 | 8 | pub struct EditorLine { 9 | text_area: TextArea, 10 | is_active: bool, 11 | 12 | cached_index: usize, 13 | cached_label_width: usize, 14 | } 15 | 16 | // state methods 17 | impl EditorLine { 18 | #[inline] 19 | pub fn is_at_line_start(&self) -> io::Result { 20 | Ok(self.text_area.state_left()?.is_at_area_start) 21 | } 22 | #[inline] 23 | pub fn is_at_line_end(&self) -> io::Result { 24 | Ok(self.text_area.state_right()?.is_at_area_end) 25 | } 26 | } 27 | 28 | // editing methods 29 | impl EditorLine { 30 | #[inline] 31 | pub fn move_cursor_to_start(&mut self, label_width: usize) -> io::Result<()> { 32 | self.update_label_width(label_width); 33 | self.text_area.move_cursor_to_start(true) 34 | } 35 | #[inline] 36 | pub fn move_cursor_to_end(&mut self, label_width: usize) -> io::Result<()> { 37 | self.update_label_width(label_width); 38 | self.text_area.move_cursor_to_end(true) 39 | } 40 | #[inline] 41 | pub fn move_cursor_horizontal(&mut self, dir: Direction) -> io::Result<()> { 42 | self.text_area.move_cursor_horizontal(dir, true) 43 | } 44 | 45 | #[inline] 46 | pub fn jump_to_word_edge(&mut self, dir: Direction) -> io::Result<()> { 47 | self.text_area.jump_to_word_edge(dir, true) 48 | } 49 | 50 | #[inline] 51 | pub fn insert_char(&mut self, ch: char) -> io::Result<()> { 52 | self.text_area.insert_char(ch, true) 53 | } 54 | #[inline] 55 | pub fn delete_char(&mut self) -> io::Result> { 56 | self.text_area.delete_char(true) 57 | } 58 | } 59 | 60 | impl EditorLine { 61 | pub fn new(width: usize, is_active: bool) -> Self { 62 | Self { 63 | text_area: TextArea::new(width, 1), 64 | is_active, 65 | 66 | cached_index: 0, 67 | cached_label_width: width, 68 | } 69 | } 70 | 71 | pub fn render(&mut self, index: usize, label_width: usize) -> io::Result<()> { 72 | (self.cached_index, self.cached_label_width) = (index, label_width); 73 | self.render_label()?; 74 | self.text_area.margin_left = label_width; 75 | self.text_area.render()?; 76 | 77 | let saved_col = Cursor::pos_col()?; 78 | Cursor::move_to_col(Terminal::width() - 1)?; 79 | print!(" "); 80 | Cursor::move_to_col(saved_col)?; 81 | return Ok(()); 82 | } 83 | 84 | pub fn find_all(&self, pat: &str) -> Option> { 85 | let mut text = self.content(); 86 | let mut pos_offset = 0; 87 | let mut result_vec = vec![]; 88 | while let Some(pos) = text.find(pat) { 89 | result_vec.push(pos + pos_offset); 90 | pos_offset += pos + pat.len(); 91 | text = &text[(pos + pat.len())..]; 92 | } 93 | 94 | if result_vec.is_empty() { 95 | return None; 96 | } else { 97 | return Some(result_vec); 98 | } 99 | } 100 | 101 | fn render_label(&self) -> io::Result<()> { 102 | let saved_cursor_pos = Cursor::pos_col()?; 103 | Cursor::move_to_col(0)?; 104 | 105 | let index_width = number_bit_count(self.cached_index); 106 | let space_width = self.cached_label_width - index_width; 107 | let line_label_str = format!("{}{}", self.cached_index, " ".repeat(space_width)); 108 | let line_label_styled = if self.is_active { 109 | EditorColor::line_active_style(&*line_label_str) 110 | } else { 111 | EditorColor::line_disabled_style(&*line_label_str) 112 | }; 113 | print!("{}", line_label_styled); 114 | Cursor::move_to_col(saved_cursor_pos)?; 115 | return Ok(()); 116 | } 117 | pub fn active(&mut self) -> io::Result<()> { 118 | self.is_active = true; 119 | self.render_label()?; 120 | return Ok(()); 121 | } 122 | pub fn disable(&mut self) -> io::Result<()> { 123 | self.is_active = false; 124 | self.render_label()?; 125 | return Ok(()); 126 | } 127 | 128 | #[inline] 129 | fn update_label_width(&mut self, new_width: usize) { 130 | self.text_area.margin_left = new_width; 131 | } 132 | 133 | #[inline] 134 | pub fn cursor_pos(&self) -> io::Result { 135 | self.text_area.cursor_pos() 136 | } 137 | 138 | #[inline] 139 | pub fn content(&self) -> &str { 140 | self.text_area.content() 141 | } 142 | 143 | #[inline] 144 | pub fn push_str(&mut self, str: &str) { 145 | self.text_area.push_str(str); 146 | } 147 | 148 | #[inline] 149 | pub fn truncate(&mut self) -> io::Result { 150 | self.text_area.truncate() 151 | } 152 | 153 | #[inline] 154 | pub fn len(&self) -> usize { 155 | self.text_area.len() 156 | } 157 | } 158 | 159 | #[test] 160 | fn editorline_find_all_test() { 161 | let mut line = EditorLine::new(0, false); 162 | line.push_str("abc abc abc"); 163 | 164 | assert_eq!(line.find_all("abc"), Some(vec![0, 5, 10])); 165 | } 166 | -------------------------------------------------------------------------------- /src/editor/core/mod.rs: -------------------------------------------------------------------------------- 1 | mod color; 2 | mod dashboard; 3 | mod event; 4 | mod history; 5 | mod init; 6 | mod line; 7 | mod state; 8 | 9 | use std::{fs, io, path::Path}; 10 | 11 | use crossterm::{ 12 | event::{KeyCode, KeyEvent, KeyModifiers}, 13 | execute, 14 | style::Stylize, 15 | terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 16 | }; 17 | 18 | use crate::utils::{number_bit_count, Cursor, Terminal}; 19 | 20 | use dashboard::EditorDashboard; 21 | use init::EditorInit; 22 | use line::EditorLine; 23 | pub use state::EditorState; 24 | 25 | use self::{ 26 | color::EditorColor, 27 | event::{EditorEvent, EditorOperation}, 28 | history::EditorHistory, 29 | }; 30 | 31 | use super::{components::{FileOpener, Finder}, direction::Direction}; 32 | use super::{ 33 | components::{EditorComponentManager, FileSaver, LineComponent, Positioner}, 34 | cursor_pos::EditorCursorPos, 35 | }; 36 | 37 | pub struct Editor { 38 | lines: Vec, 39 | index: usize, // current editing line index 40 | 41 | overflow_top: usize, 42 | overflow_bottom: usize, 43 | 44 | components: EditorComponentManager, 45 | history: EditorHistory, 46 | dashboard: EditorDashboard, 47 | } 48 | 49 | // base value calculating methods 50 | impl Editor { 51 | #[inline] 52 | fn label_width(&self) -> usize { 53 | // returns the longest line label width at left-side 54 | return number_bit_count(self.lines.len()) + 1; 55 | } 56 | 57 | #[inline] 58 | fn label_width_with(&self, value: usize) -> usize { 59 | // calculate label_width with inputed value 60 | return number_bit_count(value) + 1; 61 | } 62 | 63 | #[inline] 64 | fn visible_area_height(&self) -> usize { 65 | let term_height = Terminal::height(); 66 | // `2` here means the top and bottom border. 67 | return term_height - 2; 68 | } 69 | } 70 | 71 | // editing methods 72 | impl Editor { 73 | fn render_all(&mut self) -> io::Result<()> { 74 | Cursor::save_pos()?; 75 | Cursor::move_to_row(1)?; 76 | Cursor::move_to_col(0)?; 77 | 78 | let label_width = self.label_width(); 79 | let line_count = self.lines.len(); 80 | 81 | let line_range = self.overflow_top..(line_count - self.overflow_bottom); 82 | let lines_to_render = &mut self.lines[line_range.clone()]; 83 | let line_indices = line_range.map(|i| i + 1).collect::>(); 84 | 85 | let iter = line_indices.into_iter().zip(lines_to_render.iter_mut()); 86 | for (index, line) in iter { 87 | line.render(index, label_width)?; 88 | Cursor::down(1)?; 89 | } 90 | 91 | // initialize the unused lines 92 | let lines_to_render_count = lines_to_render.len(); 93 | let visible_area_height = self.visible_area_height(); 94 | if lines_to_render_count < visible_area_height { 95 | let diff = visible_area_height - lines_to_render_count; 96 | for _ in 0..diff { 97 | Cursor::move_to_col(0)?; 98 | Terminal::clear_after_cursor()?; 99 | print!("{}", " ".repeat(label_width).on_grey()); 100 | Cursor::down(1)?; 101 | } 102 | } 103 | Cursor::restore_pos()?; 104 | return Ok(()); 105 | } 106 | 107 | fn reset_cursor_pos(&mut self) -> io::Result<()> { 108 | Cursor::move_to_row(1)?; 109 | let label_width = self.label_width(); 110 | self.lines[0].move_cursor_to_start(label_width)?; 111 | 112 | self.index = 1; 113 | self.overflow_top = 0; 114 | self.overflow_bottom = 0; 115 | return Ok(()); 116 | } 117 | 118 | fn move_cursor_horizontal(&mut self, dir: Direction) -> io::Result<()> { 119 | let label_width = self.label_width(); 120 | let line_count = self.lines.len(); 121 | let current_line = &mut self.lines[self.index - 1]; 122 | 123 | match dir { 124 | Direction::Left => { 125 | if current_line.is_at_line_start()? && self.index > 1 { 126 | self.move_cursor_vertical(Direction::Up)?; 127 | let current_line = self.lines.get_mut(self.index - 1).unwrap(); 128 | current_line.move_cursor_to_end(label_width)?; 129 | return Ok(()); 130 | } 131 | } 132 | Direction::Right => { 133 | if current_line.is_at_line_end()? && self.index < line_count { 134 | self.move_cursor_vertical(Direction::Down)?; 135 | let current_line = self.lines.get_mut(self.index - 1).unwrap(); 136 | current_line.move_cursor_to_start(label_width)?; 137 | return Ok(()); 138 | } 139 | } 140 | _ => unreachable!(), 141 | } 142 | current_line.move_cursor_horizontal(dir)?; 143 | return Ok(()); 144 | } 145 | fn move_cursor_vertical(&mut self, dir: Direction) -> io::Result<()> { 146 | let is_at_first_line = self.index == 1; 147 | let is_at_last_line = self.index == self.lines.len(); 148 | if (is_at_first_line && dir == Direction::Up) || (is_at_last_line && dir == Direction::Down) 149 | { 150 | return Ok(()); 151 | } 152 | 153 | self.lines[self.index - 1].disable()?; // disable current line 154 | let (cursor_pos_row, cursor_pos_col) = (Cursor::pos_row()?, Cursor::pos_col()?); 155 | let label_width = self.label_width(); 156 | let target_line = match dir { 157 | Direction::Up => { 158 | let is_at_top_side = cursor_pos_row == 1; 159 | self.index -= 1; 160 | if is_at_top_side { 161 | self.overflow_top -= 1; 162 | self.overflow_bottom += 1; 163 | } else { 164 | Cursor::up(1)?; 165 | } 166 | self.lines.get_mut(self.index - 1).unwrap() 167 | } 168 | Direction::Down => { 169 | let is_at_bottom_side = cursor_pos_row == Terminal::height() - 2; 170 | self.index += 1; 171 | if is_at_bottom_side { 172 | if self.lines.len() == self.visible_area_height() { 173 | self.overflow_bottom += 1; 174 | } else { 175 | self.overflow_top += 1; 176 | self.overflow_bottom -= 1; 177 | } 178 | } else { 179 | Cursor::down(1)?; 180 | } 181 | self.lines.get_mut(self.index - 1).unwrap() 182 | } 183 | _ => unreachable!(), 184 | }; 185 | // if target_line is shorter than current line 186 | if cursor_pos_col > target_line.len() + label_width { 187 | Cursor::left(cursor_pos_col - label_width - target_line.len())?; 188 | } 189 | target_line.active()?; 190 | return Ok(()); 191 | } 192 | 193 | fn insert_line(&mut self) -> io::Result<()> { 194 | let label_width = self.label_width_with(self.lines.len() + 1); 195 | let current_line = &mut self.lines[self.index - 1]; 196 | 197 | let is_at_line_end = current_line.is_at_line_end()?; 198 | let mut new_line = EditorLine::new(label_width, true); 199 | if !is_at_line_end { 200 | // when input Enter, if cursor is not at line end, 201 | // truncate current line and push truncated string 202 | // into the new line. 203 | let truncated_str = current_line.truncate()?; 204 | new_line.push_str(&truncated_str); 205 | } 206 | current_line.disable()?; 207 | new_line.move_cursor_to_start(label_width)?; 208 | 209 | // insert new line 210 | let insert_pos = Cursor::pos_row()? + self.overflow_top; 211 | self.lines.insert(insert_pos, new_line); 212 | 213 | self.index += 1; 214 | // scroll 215 | if self.lines.len() > self.visible_area_height() { 216 | self.overflow_top += 1; 217 | } else { 218 | Cursor::down(1)?; 219 | } 220 | 221 | self.render_all()?; 222 | return Ok(()); 223 | } 224 | 225 | fn insert_char(&mut self, ch: char) -> io::Result<()> { 226 | let current_line = &mut self.lines[self.index - 1]; 227 | current_line.insert_char(ch)?; 228 | return Ok(()); 229 | } 230 | 231 | fn delete_line(&mut self) -> io::Result<()> { 232 | let label_width = self.label_width_with(self.lines.len() - 1); 233 | let (previous_line, deleted_line) = { 234 | let remove_pos = Cursor::pos_row()? + self.overflow_top - 1; 235 | let removed_line = self.lines.remove(remove_pos); 236 | let previous_line = self.lines.get_mut(remove_pos - 1); 237 | (previous_line, removed_line) 238 | }; 239 | if let Some(line) = previous_line { 240 | line.push_str(deleted_line.content()); 241 | line.move_cursor_to_end(label_width)?; 242 | line.active()?; 243 | 244 | for _ in 0..deleted_line.len() { 245 | line.move_cursor_horizontal(Direction::Left)?; 246 | } 247 | } 248 | self.index -= 1; 249 | // scroll 250 | let is_overflowed = self.lines.len() >= self.visible_area_height(); 251 | if is_overflowed && self.overflow_top > 0 { 252 | self.overflow_top -= 1; 253 | } else { 254 | Cursor::up(1)?; 255 | } 256 | // rerender 257 | self.render_all()?; 258 | return Ok(()); 259 | } 260 | 261 | fn delete(&mut self) -> io::Result<()> { 262 | let cursor_pos = Cursor::pos_col()?; 263 | let label_width = self.label_width(); 264 | if cursor_pos == label_width && self.index == 1 { 265 | // when at the start of the first line. 266 | return Ok(()); 267 | } 268 | 269 | let pos_before = self.cursor_pos()?; 270 | let current_line = &mut self.lines[self.index - 1]; 271 | 272 | if current_line.is_at_line_start()? { 273 | self.append_event(EditorOperation::DeleteLine, |e| e.delete_line())?; 274 | } else { 275 | let deleted_ch = current_line.delete_char()?; 276 | let pos_after = self.cursor_pos()?; 277 | 278 | let ch = deleted_ch.unwrap(); 279 | self.history.append(EditorEvent { 280 | op: EditorOperation::DeleteChar(ch), 281 | pos_before, 282 | pos_after, 283 | }); 284 | } 285 | return Ok(()); 286 | } 287 | 288 | fn replace(&mut self, count: usize, to: &str) -> io::Result<()> { 289 | let current_line = &mut self.lines[self.index - 1]; 290 | for _ in 0..count { 291 | current_line.move_cursor_horizontal(Direction::Right)?; 292 | current_line.delete_char()?; 293 | } 294 | 295 | for ch in to.chars().rev() { 296 | current_line.insert_char(ch)?; 297 | current_line.move_cursor_horizontal(Direction::Left)?; 298 | } 299 | return Ok(()); 300 | } 301 | 302 | // --- --- --- --- --- --- 303 | 304 | fn exec_operation(&mut self, op: EditorOperation) -> io::Result<()> { 305 | match op { 306 | EditorOperation::InsertChar(ch) => self.insert_char(ch)?, 307 | EditorOperation::DeleteChar(_) => { 308 | let current_line = &mut self.lines[self.index - 1]; 309 | current_line.delete_char()?; 310 | } 311 | EditorOperation::InsertLine => self.insert_line()?, 312 | EditorOperation::DeleteLine => self.delete_line()?, 313 | 314 | EditorOperation::Replace(from, to) => { 315 | self.replace(from.len(), to.as_str())?; 316 | } 317 | } 318 | return Ok(()); 319 | } 320 | 321 | fn append_event( 322 | &mut self, 323 | op: EditorOperation, 324 | operation_callback: impl Fn(&mut Editor) -> io::Result<()>, 325 | ) -> io::Result<()> { 326 | let pos_before = self.cursor_pos()?; 327 | operation_callback(self)?; 328 | let pos_after = self.cursor_pos()?; 329 | 330 | self.history.append(EditorEvent { 331 | op, 332 | pos_before, 333 | pos_after, 334 | }); 335 | return Ok(()); 336 | } 337 | 338 | fn undo(&mut self) -> io::Result<()> { 339 | if let Some(ev) = self.history.undo() { 340 | let target_pos = ev.pos_after; 341 | let target_op = ev.op.rev(); 342 | 343 | self.jump_to(target_pos)?; 344 | self.exec_operation(target_op)?; 345 | } 346 | return Ok(()); 347 | } 348 | fn redo(&mut self) -> io::Result<()> { 349 | if let Some(ev) = self.history.redo() { 350 | let target_pos = ev.pos_before; 351 | let target_op = ev.op.clone(); 352 | 353 | self.jump_to(target_pos)?; 354 | self.exec_operation(target_op)?; 355 | } 356 | return Ok(()); 357 | } 358 | } 359 | 360 | // cursor position controller 361 | impl Editor { 362 | fn cursor_pos(&self) -> io::Result { 363 | let current_line = &self.lines[self.index - 1]; 364 | let col = current_line.cursor_pos()? + 1; 365 | let row = Cursor::pos_row()? + self.overflow_top; 366 | return Ok(EditorCursorPos { row, col }); 367 | } 368 | 369 | fn check_cursor_pos(&self, pos: EditorCursorPos) -> bool { 370 | let EditorCursorPos { row, col } = pos; 371 | let is_row_overflow = row > self.lines.len(); 372 | let is_col_overflow = if is_row_overflow { 373 | true 374 | } else { 375 | let target_line = &self.lines[row - 1]; 376 | col == 0 || col > target_line.len() + 1 377 | }; 378 | return !is_row_overflow && !is_col_overflow; 379 | } 380 | 381 | fn jump_to(&mut self, target_pos: EditorCursorPos) -> io::Result<()> { 382 | if target_pos == self.cursor_pos()? { 383 | return Ok(()); 384 | } 385 | 386 | // move to target row 387 | let target_row = target_pos.row; 388 | let (dir, diff) = if target_row > self.index { 389 | (Direction::Down, target_row - self.index) 390 | } else { 391 | (Direction::Up, self.index - target_row) 392 | }; 393 | for _ in 0..diff { 394 | self.move_cursor_vertical(dir)?; 395 | } 396 | // is not first and last line 397 | if self.index != 1 && self.index != self.lines.len() { 398 | self.move_cursor_vertical(dir)?; 399 | self.move_cursor_vertical(dir.rev())?; 400 | } 401 | 402 | // move to target col 403 | let label_width = self.label_width(); 404 | let target_line = &mut self.lines[target_row - 1]; 405 | target_line.move_cursor_to_start(label_width)?; 406 | for _ in 0..(target_pos.col - 1) { 407 | target_line.move_cursor_horizontal(Direction::Right)?; 408 | } 409 | self.render_all()?; 410 | return Ok(()); 411 | } 412 | } 413 | 414 | // callback resolver methods 415 | impl Editor { 416 | // operate editor when using component. 417 | fn component_exec( 418 | &mut self, 419 | component_callback: impl Fn(&mut Editor) -> io::Result<()>, 420 | ) -> io::Result<()> { 421 | Cursor::restore_pos()?; 422 | component_callback(self)?; 423 | Cursor::save_pos()?; 424 | 425 | // reopen current component 426 | match self.dashboard.state() { 427 | EditorState::Saving => self.components.file_saver.open()?, 428 | EditorState::Positioning => self.components.positioner.open()?, 429 | EditorState::Finding => self.components.finder.open()?, 430 | EditorState::Replacing => self.components.replacer.open()?, 431 | _ => unreachable!(), 432 | } 433 | return Ok(()); 434 | } 435 | 436 | #[inline] 437 | fn dashboard_cursor_pos_refresh(&mut self) -> io::Result<()> { 438 | let current_cursor_pos = self.cursor_pos()?; 439 | self.dashboard.set_cursor_pos(current_cursor_pos)?; 440 | return Ok(()); 441 | } 442 | 443 | fn callbacks_resolve(&mut self, key: KeyEvent) -> io::Result<()> { 444 | match self.dashboard.state() { 445 | EditorState::Saving if FileSaver::is_save_callback_key(key) => { 446 | self.dashboard.set_state(EditorState::Saved)?; 447 | } 448 | EditorState::Opening if FileOpener::is_open_file_callback_key(key) => { 449 | self.toggle_state(EditorState::Opening)?; 450 | let path = self.components.file_opener.get_file_path().to_owned(); 451 | self.read_file(&path)?; 452 | self.render_all()?; 453 | self.reset_cursor_pos()?; 454 | } 455 | EditorState::Positioning if Positioner::is_positioning_key(key) => { 456 | self.toggle_state(EditorState::Positioning)?; 457 | 458 | let target_pos = self.components.positioner.get_target(); 459 | if self.check_cursor_pos(target_pos) { 460 | self.jump_to(target_pos)?; 461 | self.dashboard.set_cursor_pos(target_pos)?; 462 | } 463 | } 464 | EditorState::Finding => { 465 | let option_target_pos = if Finder::is_finding_key(key) { 466 | if self.components.finder.is_empty() { 467 | let target_text = self.components.finder.content(); 468 | if let Some(pos_list) = self.search(target_text) { 469 | self.components.finder.set_matches(pos_list); 470 | } 471 | } 472 | self.components.finder.next() 473 | } else if Finder::is_reverse_finding_key(key) { 474 | self.components.finder.previous() 475 | } else { 476 | None 477 | }; 478 | 479 | if let Some(pos) = option_target_pos { 480 | let pos = *pos; 481 | self.component_exec(|e| e.jump_to(pos))?; 482 | } 483 | } 484 | EditorState::Replacing => { 485 | fn replace_pos_processor( 486 | last_event: &EditorEvent, 487 | current_pos: EditorCursorPos, 488 | next_pos: &mut EditorCursorPos, 489 | ) { 490 | let EditorEvent { 491 | op: EditorOperation::Replace(from, to), 492 | pos_before, 493 | .. 494 | } = last_event 495 | else { 496 | return; 497 | }; 498 | 499 | if pos_before.row == next_pos.row { 500 | let text_diff = from.len().abs_diff(to.len()); 501 | let col_diff = next_pos.col - current_pos.col; 502 | next_pos.col = pos_before.col + col_diff; 503 | if from.len() > to.len() { 504 | next_pos.col -= text_diff; 505 | } else { 506 | next_pos.col += text_diff; 507 | } 508 | } 509 | } 510 | 511 | if self.components.replacer.is_search_key(key) { 512 | let target_content = self.components.replacer.search_text(); 513 | if let Some(pos_list) = self.search(target_content) { 514 | let replacer = &mut self.components.replacer; 515 | replacer.search_handler(pos_list)?; 516 | 517 | let first_target_pos = *replacer.first().unwrap(); 518 | self.component_exec(|e| e.jump_to(first_target_pos))?; 519 | } 520 | } else if self.components.replacer.is_next_key(key) { 521 | // jump to next position 522 | let next_pos = self.components.replacer.next(); 523 | if let Some(pos) = next_pos { 524 | let pos = *pos; 525 | self.component_exec(|e| e.jump_to(pos))?; 526 | } 527 | } else if self.components.replacer.is_replace_one_key(key) { 528 | self.component_exec(|e| { 529 | let replacer = &mut e.components.replacer; 530 | let current_pos = *replacer.current(); 531 | 532 | if let Some(mut next_pos) = replacer.next().cloned() { 533 | if let Some(ev) = e.history.previous_event() { 534 | replace_pos_processor(ev, current_pos, &mut next_pos); 535 | } 536 | 537 | let replace_op = EditorOperation::Replace( 538 | replacer.search_text().to_owned(), 539 | replacer.replace_text().to_owned(), 540 | ); 541 | let replace_count = replacer.search_text().len(); 542 | let replace_text = &replacer.replace_text().to_owned(); 543 | replacer.replace_handler(); 544 | 545 | e.jump_to(next_pos)?; 546 | e.append_event(replace_op, |e| e.replace(replace_count, replace_text))?; 547 | } 548 | return Ok(()); 549 | })?; 550 | } else if self.components.replacer.is_replace_all_key(key) { 551 | // close replacer 552 | self.toggle_state(EditorState::Replacing)?; 553 | 554 | let replacer = &mut self.components.replacer; 555 | let replace_count = replacer.search_text().len(); 556 | let replace_text = &replacer.replace_text().to_owned(); 557 | let replace_op = EditorOperation::Replace( 558 | replacer.search_text().to_owned(), 559 | replacer.replace_text().to_owned(), 560 | ); 561 | replacer.replace_handler(); 562 | 563 | let mut current_pos = *replacer.current(); 564 | 565 | while let Some(mut next_pos) = self.components.replacer.next().cloned() { 566 | if let Some(ev) = self.history.previous_event() { 567 | replace_pos_processor(ev, current_pos, &mut next_pos); 568 | } 569 | 570 | self.jump_to(next_pos)?; 571 | self.append_event(replace_op.clone(), |e| { 572 | e.replace(replace_count, replace_text) 573 | })?; 574 | 575 | current_pos = *self.components.replacer.current(); 576 | } 577 | } 578 | } 579 | _ => {} 580 | } 581 | return Ok(()); 582 | } 583 | } 584 | 585 | // Non-editing methods 586 | impl Editor { 587 | pub fn new() -> Self { 588 | Self { 589 | lines: vec![], 590 | index: 1, 591 | 592 | overflow_top: 0, 593 | overflow_bottom: 0, 594 | 595 | components: EditorComponentManager::new(), 596 | history: EditorHistory::new(), 597 | dashboard: EditorDashboard::new(), 598 | } 599 | } 600 | 601 | pub fn init(&mut self) -> io::Result<()> { 602 | execute!(io::stdout(), EnterAlternateScreen)?; 603 | enable_raw_mode()?; 604 | Cursor::move_to_left_top()?; 605 | 606 | EditorInit::display_title(); 607 | EditorInit::display_border()?; 608 | self.dashboard.render()?; 609 | 610 | // lines.is_empty() == true -> no file reading 611 | if self.lines.is_empty() { 612 | // `2` here is the width of line label ("1 ") in terminal. 613 | self.lines.push(EditorLine::new(2, true)); 614 | } 615 | 616 | // move cursor to start of first line 617 | Cursor::move_to_row(1)?; 618 | let label_width = self.label_width(); 619 | self.lines 620 | .first_mut() 621 | .unwrap() 622 | .move_cursor_to_start(label_width)?; 623 | 624 | self.render_all()?; 625 | Terminal::flush()?; 626 | return Ok(()); 627 | } 628 | 629 | pub fn close(&self) -> io::Result<()> { 630 | disable_raw_mode()?; 631 | execute!(io::stdout(), LeaveAlternateScreen)?; 632 | return Ok(()); 633 | } 634 | 635 | #[inline] 636 | pub fn set_accent_color(color: &str) { 637 | EditorColor::set_accent_color(color); 638 | } 639 | 640 | pub fn read_file(&mut self, path: &str) -> io::Result<()> { 641 | self.components.file_saver.set_path(path); 642 | if !Path::new(path).exists() { 643 | return Ok(()); 644 | } 645 | 646 | let file_read_res = fs::read_to_string(path); 647 | match file_read_res { 648 | Ok(content) => { 649 | let file_lines = content.lines(); 650 | let line_count = file_lines.clone().count(); 651 | let visible_area_height = self.visible_area_height(); 652 | let label_width = self.label_width_with(line_count); 653 | 654 | // set `overflow_bottom` 655 | if line_count > visible_area_height { 656 | self.overflow_bottom = line_count - visible_area_height; 657 | } 658 | 659 | self.lines = file_lines 660 | .map(|l| { 661 | let mut new_line = EditorLine::new(label_width, false); 662 | new_line.push_str(l); 663 | new_line 664 | }) 665 | .collect(); 666 | } 667 | Err(_) => { 668 | self.close()?; 669 | panic!("File reading failed!") 670 | } 671 | }; 672 | return Ok(()); 673 | } 674 | 675 | fn search(&self, target: &str) -> Option> { 676 | let mut result_pos_list = Vec::::new(); 677 | for (index, line) in self.lines.iter().enumerate() { 678 | if let Some(pos_list) = line.find_all(target) { 679 | for pos in pos_list { 680 | result_pos_list.push(EditorCursorPos { 681 | row: index + 1, 682 | col: pos + 1, 683 | }) 684 | } 685 | } 686 | } 687 | 688 | if !result_pos_list.is_empty() { 689 | return Some(result_pos_list); 690 | } else { 691 | return None; 692 | } 693 | } 694 | 695 | fn content(&self) -> String { 696 | let mut buf = String::new(); 697 | let mut iter = self.lines.iter(); 698 | while let Some(line) = iter.next() { 699 | buf += line.content(); 700 | if iter.len() > 0 { 701 | buf += "\r\n"; 702 | } 703 | } 704 | return buf; 705 | } 706 | 707 | // --- --- --- --- --- --- 708 | 709 | fn toggle_state(&mut self, new_state: EditorState) -> io::Result<()> { 710 | match self.dashboard.state() { 711 | // set mode 712 | EditorState::Saved | EditorState::Modified if !self.components.use_line_component => { 713 | Cursor::save_pos()?; 714 | self.components.use_line_component = true; 715 | self.dashboard.set_state(new_state)?; 716 | 717 | match new_state { 718 | EditorState::Saving => { 719 | let current_content = self.content(); 720 | let file_saver = &mut self.components.file_saver; 721 | file_saver.set_content(current_content); 722 | file_saver.open()?; 723 | } 724 | EditorState::Opening => { 725 | let file_opener = &mut self.components.file_opener; 726 | file_opener.open()?; 727 | } 728 | EditorState::Positioning => { 729 | let current_cursor_pos = self.cursor_pos()?; 730 | let positioner = &mut self.components.positioner; 731 | positioner.set_cursor_pos(current_cursor_pos); 732 | positioner.open()?; 733 | } 734 | EditorState::Finding => { 735 | let finder = &mut self.components.finder; 736 | finder.clear(); 737 | finder.open()?; 738 | } 739 | EditorState::Replacing => { 740 | let replacer = &mut self.components.replacer; 741 | replacer.reset(); 742 | replacer.open()?; 743 | } 744 | EditorState::ReadingHelpMsg => { 745 | let helper = &self.components.helper; 746 | helper.open()?; 747 | } 748 | _ => unreachable!(), 749 | } 750 | } 751 | // restore to normal mode 752 | s if s == new_state && self.components.use_line_component => { 753 | // restore the covered line 754 | let label_width = self.label_width(); 755 | let covered_pos = Cursor::pos_row()? + self.overflow_top - 1; 756 | match self.lines.get_mut(covered_pos) { 757 | Some(covered_line) => covered_line.render(covered_pos + 1, label_width)?, 758 | None => { 759 | // render blank line 760 | let label_width = self.label_width(); 761 | Cursor::move_to_col(0)?; 762 | Terminal::clear_after_cursor()?; 763 | print!("{}", " ".repeat(label_width).on_grey()); 764 | } 765 | } 766 | Cursor::restore_pos()?; 767 | self.dashboard.restore_state()?; 768 | self.components.use_line_component = false; 769 | } 770 | _ => {} 771 | } 772 | return Ok(()); 773 | } 774 | 775 | pub fn cycle(&mut self) -> io::Result<()> { 776 | loop { 777 | let Some(key) = Terminal::get_key() else { 778 | continue; 779 | }; 780 | 781 | // ctrl shotcuts 782 | if key.modifiers == KeyModifiers::CONTROL { 783 | match key.code { 784 | KeyCode::Left | KeyCode::Right => { 785 | let current_line = &mut self.lines[self.index - 1]; 786 | current_line.jump_to_word_edge(Direction::from(key.code))?; 787 | } 788 | KeyCode::Char(ch) => match ch { 789 | 'z' => self.undo()?, 790 | 'y' => self.redo()?, 791 | 's' => self.toggle_state(EditorState::Saving)?, 792 | 'o' => self.toggle_state(EditorState::Opening)?, 793 | 'h' => self.toggle_state(EditorState::ReadingHelpMsg)?, 794 | 'g' => self.toggle_state(EditorState::Positioning)?, 795 | 'f' => self.toggle_state(EditorState::Finding)?, 796 | 'r' => self.toggle_state(EditorState::Replacing)?, 797 | _ => {} 798 | }, 799 | 800 | // ignore other Ctrl shotcuts 801 | _ => {} 802 | } 803 | 804 | if !self.components.use_line_component || !self.components.use_screen_component { 805 | continue; 806 | } 807 | } 808 | 809 | if self.components.use_line_component || self.components.use_screen_component { 810 | let current_state = self.dashboard.state(); 811 | 812 | if key.code == KeyCode::Esc { 813 | // use key `Esc` to restore to normal mode 814 | self.toggle_state(current_state)?; 815 | continue; 816 | } 817 | self.components.resolve(current_state, key)?; 818 | self.callbacks_resolve(key)?; 819 | continue; 820 | } 821 | 822 | // will enter matches in normal mode 823 | match key.code { 824 | // input `Escape` to exit 825 | KeyCode::Esc => break, 826 | 827 | KeyCode::Up | KeyCode::Down => { 828 | self.move_cursor_vertical(Direction::from(key.code))?; 829 | self.render_all()?; 830 | } 831 | KeyCode::Left | KeyCode::Right => { 832 | self.move_cursor_horizontal(Direction::from(key.code))?; 833 | } 834 | KeyCode::Backspace | KeyCode::Enter | KeyCode::Char(_) => { 835 | self.dashboard.set_state(EditorState::Modified)?; 836 | match key.code { 837 | KeyCode::Backspace => self.delete()?, 838 | KeyCode::Enter => { 839 | self.append_event(EditorOperation::InsertLine, |e| e.insert_line())?; 840 | } 841 | KeyCode::Char(ch) => { 842 | if !ch.is_ascii() { 843 | // avoid Non-ASCII characters 844 | continue; 845 | } 846 | self.append_event(EditorOperation::InsertChar(ch), |e| { 847 | e.insert_char(ch) 848 | })?; 849 | } 850 | _ => unreachable!(), 851 | } 852 | } 853 | _ => {} 854 | } 855 | self.dashboard_cursor_pos_refresh()?; 856 | Terminal::flush()?; 857 | } 858 | self.close()?; 859 | return Ok(()); 860 | } 861 | } 862 | -------------------------------------------------------------------------------- /src/editor/core/state.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(PartialEq, Clone, Copy)] 4 | pub enum EditorState { 5 | Saved, 6 | Modified, 7 | 8 | // states for components 9 | Saving, 10 | Opening, 11 | Positioning, 12 | Finding, 13 | Replacing, 14 | 15 | ReadingHelpMsg 16 | } 17 | 18 | impl EditorState { 19 | pub fn is_component_state(&self) -> bool { 20 | !matches!(self, Self::Saved | Self::Modified) 21 | } 22 | } 23 | 24 | impl fmt::Display for EditorState { 25 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 26 | let str = match self { 27 | Self::Saved => "Saved", 28 | Self::Modified => "Modified", 29 | 30 | Self::Saving => "Saving", 31 | Self::Opening => "Opening", 32 | Self::Positioning => "Positioning", 33 | Self::Finding => "Finding", 34 | Self::Replacing => "Replacing", 35 | 36 | Self::ReadingHelpMsg => "Reading", 37 | }; 38 | write!(f, "{}", str) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/editor/cursor_pos.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, io}; 2 | 3 | use crate::utils::Cursor; 4 | 5 | // use to indicate virtual cursor position 6 | // in editing area. 7 | #[derive(Debug, PartialEq, Clone, Copy)] 8 | pub struct EditorCursorPos { 9 | pub row: usize, 10 | pub col: usize, 11 | } 12 | 13 | impl EditorCursorPos { 14 | pub fn short_display(&self) -> String { 15 | format!("{}: {}", self.row, self.col) 16 | } 17 | 18 | #[allow(unused_assignments)] 19 | pub fn parse(value: &str) -> Option { 20 | fn str_to_num(s: &str) -> Option { 21 | match s.parse::() { 22 | Ok(num) => Some(num), 23 | Err(_) => None, 24 | } 25 | } 26 | 27 | let chars = value.chars(); 28 | let mut number_str = String::new(); 29 | let (mut row, mut col) = (1, 1); 30 | 31 | for ch in chars { 32 | if ch.is_ascii_digit() { 33 | number_str.push(ch); 34 | } else { 35 | match ch { 36 | ',' | ':' => { 37 | row = str_to_num(&number_str)?; 38 | number_str.clear(); 39 | } 40 | ' ' => continue, 41 | _ => return None, 42 | } 43 | } 44 | } 45 | col = str_to_num(&number_str)?; 46 | return Some(EditorCursorPos { row, col }); 47 | } 48 | } 49 | 50 | // default display 51 | impl fmt::Display for EditorCursorPos { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | write!(f, "Ln {}, Col {}", self.row, self.col) 54 | } 55 | } 56 | 57 | // --- --- --- --- --- --- 58 | 59 | // use to temporarily save and restore cursor. 60 | pub struct TerminalCursorPos { 61 | pub row: usize, 62 | pub col: usize, 63 | } 64 | 65 | impl TerminalCursorPos { 66 | #[inline] 67 | pub fn save_pos(&mut self) -> io::Result<()> { 68 | (self.row, self.col) = (Cursor::pos_row()?, Cursor::pos_col()?); 69 | return Ok(()); 70 | } 71 | 72 | #[inline] 73 | pub fn restore_pos(&self) -> io::Result<()> { 74 | Cursor::move_to_row(self.row)?; 75 | Cursor::move_to_col(self.col)?; 76 | return Ok(()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/editor/direction.rs: -------------------------------------------------------------------------------- 1 | use crossterm::event::KeyCode; 2 | 3 | #[derive(PartialEq, Clone, Copy)] 4 | pub enum Direction { 5 | Up, 6 | Down, 7 | 8 | Left, 9 | Right, 10 | } 11 | 12 | impl Direction { 13 | pub fn rev(&self) -> Self { 14 | match self { 15 | Self::Up => Self::Down, 16 | Self::Down => Self::Up, 17 | Self::Left => Self::Right, 18 | Self::Right => Self::Left, 19 | } 20 | } 21 | } 22 | 23 | impl From for Direction { 24 | fn from(value: KeyCode) -> Self { 25 | match value { 26 | KeyCode::Up => Self::Up, 27 | KeyCode::Down => Self::Down, 28 | KeyCode::Left => Self::Left, 29 | KeyCode::Right => Self::Right, 30 | _ => unreachable!(), 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/editor/mod.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | 3 | mod cursor_pos; 4 | mod direction; 5 | mod text_area; 6 | 7 | mod core; 8 | pub use self::core::Editor; 9 | -------------------------------------------------------------------------------- /src/editor/text_area.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::{event::KeyCode, style::Stylize}; 4 | 5 | use crate::utils::{Cursor, Terminal}; 6 | 7 | use super::direction::Direction; 8 | 9 | pub struct TextArea { 10 | content: String, 11 | placeholder: String, 12 | 13 | pub margin_left: usize, 14 | pub margin_right: usize, 15 | 16 | overflow_left: usize, 17 | overflow_right: usize, 18 | } 19 | 20 | pub struct TextAreaStateLeft { 21 | pub is_at_left_side: bool, 22 | pub is_at_area_start: bool, 23 | } 24 | pub struct TextAreaStateRight { 25 | pub is_at_right_side: bool, 26 | pub is_at_area_end: bool, 27 | } 28 | 29 | // state calculating methods 30 | impl TextArea { 31 | #[inline] 32 | pub fn is_at_left_side(&self) -> io::Result { 33 | return Ok(Cursor::pos_col()? == self.margin_left); 34 | } 35 | #[inline] 36 | pub fn is_at_right_side(&self) -> io::Result { 37 | return Ok(Cursor::pos_col()? == Terminal::width() - 1); 38 | } 39 | 40 | pub fn state_left(&self) -> io::Result { 41 | let is_at_left_side = self.is_at_left_side()?; 42 | let is_at_area_start = is_at_left_side && self.overflow_left == 0; 43 | return Ok(TextAreaStateLeft { 44 | is_at_left_side, 45 | is_at_area_start, 46 | }); 47 | } 48 | pub fn state_right(&self) -> io::Result { 49 | let cursor_pos_col = Cursor::pos_col()?; 50 | let is_at_right_side = self.is_at_right_side()?; 51 | let is_at_area_end = cursor_pos_col == (self.len() + self.margin_left) 52 | || cursor_pos_col == (self.len() - self.overflow_left + self.margin_left); 53 | return Ok(TextAreaStateRight { 54 | is_at_right_side, 55 | is_at_area_end, 56 | }); 57 | } 58 | } 59 | 60 | // editing methods 61 | impl TextArea { 62 | pub fn visible_area_width(&self) -> usize { 63 | let term_width = Terminal::width(); 64 | return term_width - self.margin_left - self.margin_right; 65 | } 66 | 67 | #[inline] 68 | pub fn is_editing_key(key: KeyCode) -> bool { 69 | matches!( 70 | key, 71 | KeyCode::Backspace | KeyCode::Left | KeyCode::Right | KeyCode::Char(_) 72 | ) 73 | } 74 | 75 | // returns number of continuous alphabetic char. 76 | // e.g. 77 | // in : ['a', 'b', ' ', 'c'] 78 | // out: 2 79 | // --- --- --- --- --- --- 80 | // in : [' ', 'a', 'b'] 81 | // out: 1 82 | fn continuous_word_count(chars: impl Iterator) -> usize { 83 | let counter = chars 84 | .map_while(|ch| ch.is_alphabetic().then_some(())) 85 | .count(); 86 | return counter; 87 | } 88 | 89 | fn overflow_refresh(&mut self) { 90 | let visible_area_width = self.visible_area_width(); 91 | if self.len() > visible_area_width { 92 | self.overflow_left = self.len() - visible_area_width - self.overflow_right; 93 | } else { 94 | self.overflow_left = 0; 95 | self.overflow_right = 0; 96 | } 97 | } 98 | 99 | pub fn move_cursor_to_start(&mut self, rerender: bool) -> io::Result<()> { 100 | if self.len() >= self.visible_area_width() { 101 | self.overflow_right += self.overflow_left; 102 | self.overflow_left = 0; 103 | if rerender { 104 | self.render()?; 105 | } 106 | } 107 | Cursor::move_to_col(self.margin_left)?; 108 | return Ok(()); 109 | } 110 | pub fn move_cursor_to_end(&mut self, rerender: bool) -> io::Result<()> { 111 | if self.len() >= self.visible_area_width() { 112 | Cursor::move_to_col(Terminal::width() - 1)?; 113 | self.overflow_left += self.overflow_right; 114 | self.overflow_right = 0; 115 | 116 | if rerender { 117 | self.render()?; 118 | } 119 | } else { 120 | let line_end_pos = self.margin_left + self.len(); 121 | Cursor::move_to_col(line_end_pos)?; 122 | } 123 | return Ok(()); 124 | } 125 | 126 | pub fn move_cursor_horizontal(&mut self, dir: Direction, rerender: bool) -> io::Result<()> { 127 | match dir { 128 | Direction::Left => { 129 | let state = self.state_left()?; 130 | if state.is_at_area_start { 131 | return Ok(()); 132 | } 133 | 134 | if state.is_at_left_side { 135 | self.overflow_left -= 1; 136 | self.overflow_right += 1; 137 | } else { 138 | Cursor::left(1)?; 139 | return Ok(()); // skip rerender 140 | } 141 | } 142 | Direction::Right => { 143 | let state = self.state_right()?; 144 | if state.is_at_area_end { 145 | return Ok(()); 146 | } 147 | 148 | if state.is_at_right_side { 149 | self.overflow_right -= 1; 150 | self.overflow_left += 1; 151 | } else { 152 | Cursor::right(1)?; 153 | return Ok(()); // skip rerender 154 | } 155 | } 156 | _ => unreachable!(), 157 | } 158 | if rerender { 159 | self.render()?; 160 | } 161 | return Ok(()); 162 | } 163 | 164 | pub fn jump_to_word_edge(&mut self, dir: Direction, rerender: bool) -> io::Result<()> { 165 | let cursor_pos = self.cursor_pos()?; 166 | let mut displacement = match dir { 167 | Direction::Left => { 168 | let iter = self.content()[..cursor_pos].chars().rev(); 169 | Self::continuous_word_count(iter) 170 | } 171 | Direction::Right => { 172 | let iter = self.content()[cursor_pos..].chars(); 173 | Self::continuous_word_count(iter) 174 | } 175 | _ => unreachable!(), 176 | }; 177 | 178 | // when displacement is 0 and cursor is not at left and right end 179 | if displacement == 0 180 | && !(dir == Direction::Left && self.state_left()?.is_at_area_start) 181 | && !(dir == Direction::Right && self.state_right()?.is_at_area_end) 182 | { 183 | displacement = 1; 184 | } 185 | 186 | for _ in 0..displacement { 187 | self.move_cursor_horizontal(dir, false)?; 188 | } 189 | if rerender { 190 | self.render()?; 191 | } 192 | return Ok(()); 193 | } 194 | } 195 | 196 | impl TextArea { 197 | pub fn new(margin_left: usize, margin_right: usize) -> Self { 198 | Self { 199 | content: String::new(), 200 | placeholder: String::new(), 201 | 202 | overflow_left: 0, 203 | overflow_right: 0, 204 | 205 | margin_left, 206 | margin_right, 207 | } 208 | } 209 | 210 | pub fn render(&self) -> io::Result<()> { 211 | let visible_area_width = self.visible_area_width(); 212 | 213 | let rendered_content = if self.len() == 0 && !self.placeholder.is_empty() { 214 | if self.placeholder.len() > visible_area_width { 215 | let rendered_range = 0..visible_area_width; 216 | self.placeholder[rendered_range].dim() 217 | } else { 218 | self.placeholder.as_str().dim() 219 | } 220 | } else if self.len() > visible_area_width { 221 | let rendered_range = self.overflow_left..(self.len() - self.overflow_right); 222 | self.content[rendered_range].stylize() 223 | } else { 224 | self.content.as_str().stylize() 225 | }; 226 | let remain_area_width = visible_area_width - rendered_content.content().len(); 227 | let remain_space_str = " ".repeat(remain_area_width); 228 | 229 | let saved_cursor_pos = Cursor::pos_col()?; 230 | Cursor::move_to_col(self.margin_left)?; 231 | print!("{}{}", rendered_content, remain_space_str); 232 | Cursor::move_to_col(saved_cursor_pos)?; 233 | return Ok(()); 234 | } 235 | 236 | pub fn insert_char(&mut self, ch: char, rerender: bool) -> io::Result<()> { 237 | let insert_pos = self.cursor_pos()?; 238 | self.content.insert(insert_pos, ch); 239 | 240 | if self.content.len() > self.visible_area_width() { 241 | self.overflow_left += 1; 242 | } else { 243 | Cursor::right(1)?; 244 | } 245 | if rerender { 246 | self.render()?; 247 | } 248 | return Ok(()); 249 | } 250 | 251 | pub fn delete_char(&mut self, rerender: bool) -> io::Result> { 252 | if self.state_left()?.is_at_area_start { 253 | return Ok(None); 254 | } 255 | 256 | let remove_pos = self.cursor_pos()? - 1; 257 | let removed_ch = self.content.remove(remove_pos); 258 | 259 | if self.content.len() >= self.visible_area_width() { 260 | self.overflow_left -= 1; 261 | } else { 262 | Cursor::left(1)?; 263 | } 264 | if rerender { 265 | self.render()?; 266 | } 267 | return Ok(Some(removed_ch)); 268 | } 269 | 270 | #[inline] 271 | pub fn push_str(&mut self, str: &str) { 272 | self.content.push_str(str); 273 | self.overflow_refresh(); 274 | } 275 | 276 | #[inline] 277 | pub fn set_content(&mut self, str: &str) { 278 | self.content = str.to_owned(); 279 | self.overflow_refresh(); 280 | } 281 | 282 | #[inline] 283 | pub fn set_placeholder(&mut self, str: &str) { 284 | self.placeholder = str.to_owned(); 285 | } 286 | 287 | pub fn truncate(&mut self) -> io::Result { 288 | let truncate_pos = self.cursor_pos()?; 289 | let mut res_str = String::new(); 290 | 291 | self.content[truncate_pos..].clone_into(&mut res_str); 292 | self.content.truncate(truncate_pos); 293 | self.overflow_refresh(); 294 | return Ok(res_str); 295 | } 296 | 297 | #[inline] 298 | pub fn cursor_pos(&self) -> io::Result { 299 | let value = Cursor::pos_col()? + self.overflow_left - self.margin_left; 300 | return Ok(value); 301 | } 302 | 303 | #[inline] 304 | pub fn content(&self) -> &str { 305 | &self.content 306 | } 307 | 308 | pub fn clear(&mut self) { 309 | self.overflow_left = 0; 310 | self.overflow_right = 0; 311 | self.content.clear(); 312 | } 313 | 314 | #[inline] 315 | pub fn len(&self) -> usize { 316 | self.content.len() 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_return)] 2 | 3 | mod editor; 4 | mod utils; 5 | 6 | use std::{io, path::Path}; 7 | 8 | use clap::Parser; 9 | 10 | use editor::Editor; 11 | 12 | #[derive(Parser, Debug)] 13 | #[command(name="Rusditor", version)] 14 | struct Args { 15 | file_path: Option, 16 | #[arg(short, long, long_help="Set accent color for this editor; Options: [red, blue, dark_red, dark_blue, dark_grey, dark_cyan, dark_yellow, dark_magenta]")] 17 | accent_color: Option, 18 | } 19 | 20 | fn main() -> io::Result<()> { 21 | let mut editor = Editor::new(); 22 | let args = Args::parse(); 23 | if let Some(path) = args.file_path { 24 | let file_path = Path::new(&path); 25 | if file_path.exists() { 26 | editor.read_file(&path)?; 27 | } 28 | } 29 | if let Some(color) = args.accent_color { 30 | Editor::set_accent_color(&color); 31 | } 32 | 33 | if cfg!(debug_assertions) { 34 | // debug mode: show `io::Error` message 35 | editor.init()?; 36 | editor.cycle()?; 37 | } else { 38 | // release mode: exit directly 39 | editor.init().or_else(|_| editor.close())?; 40 | editor.cycle().or_else(|_| editor.close())?; 41 | } 42 | return Ok(()); 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/cursor.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crossterm::{cursor, execute}; 4 | 5 | pub struct Cursor; 6 | 7 | impl Cursor { 8 | // return current cursor col 9 | fn position() -> io::Result<(usize, usize)> { 10 | let (col, row) = cursor::position()?; 11 | Ok((col as usize, row as usize)) 12 | } 13 | 14 | pub fn pos_col() -> io::Result { 15 | Ok(Self::position()?.0) 16 | } 17 | pub fn pos_row() -> io::Result { 18 | Ok(Self::position()?.1) 19 | } 20 | 21 | pub fn move_to_left_top() -> io::Result<()> { 22 | Self::move_to_col(0)?; 23 | Self::move_to_row(0)?; 24 | return Ok(()); 25 | } 26 | 27 | pub fn move_to_col(target_col: usize) -> io::Result<()> { 28 | execute!(io::stdout(), cursor::MoveToColumn(target_col as u16)) 29 | } 30 | pub fn move_to_row(target_row: usize) -> io::Result<()> { 31 | execute!(io::stdout(), cursor::MoveToRow(target_row as u16)) 32 | } 33 | 34 | pub fn up(cell: usize) -> io::Result<()> { 35 | execute!(io::stdout(), cursor::MoveUp(cell as u16)) 36 | } 37 | pub fn down(cell: usize) -> io::Result<()> { 38 | execute!(io::stdout(), cursor::MoveDown(cell as u16)) 39 | } 40 | pub fn left(cell: usize) -> io::Result<()> { 41 | execute!(io::stdout(), cursor::MoveLeft(cell as u16)) 42 | } 43 | pub fn right(cell: usize) -> io::Result<()> { 44 | execute!(io::stdout(), cursor::MoveRight(cell as u16)) 45 | } 46 | 47 | pub fn save_pos() -> io::Result<()> { 48 | execute!(io::stdout(), cursor::SavePosition) 49 | } 50 | pub fn restore_pos() -> io::Result<()> { 51 | execute!(io::stdout(), cursor::RestorePosition) 52 | } 53 | 54 | // pub fn hide() -> io::Result<()> { 55 | // execute!(io::stdout(), cursor::Hide) 56 | // } 57 | // pub fn show() -> io::Result<()> { 58 | // execute!(io::stdout(), cursor::Show) 59 | // } 60 | } 61 | -------------------------------------------------------------------------------- /src/utils/logger.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Display, 3 | fs::{File, OpenOptions}, 4 | io::{self, Write}, 5 | }; 6 | 7 | #[allow(dead_code)] 8 | // output something into file 9 | // this function is used to debug. 10 | pub fn log(content: T) -> io::Result<()> { 11 | File::create("log.txt")?; 12 | let mut file = OpenOptions::new().write(true).open("log.txt")?; 13 | file.write_all(format!("{}", content).as_bytes())?; 14 | file.flush()?; 15 | return Ok(()); 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/loop_traverser.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | pub struct LoopTraverser { 4 | vec: VecDeque, 5 | pub index: isize, 6 | pub cycle: bool, 7 | } 8 | 9 | impl LoopTraverser { 10 | pub fn new(is_cycle: bool) -> Self { 11 | Self { 12 | vec: VecDeque::::new(), 13 | index: -1, 14 | cycle: is_cycle, 15 | } 16 | } 17 | 18 | pub fn next(&mut self) -> Option<&T> { 19 | if self.vec.is_empty() || (!self.cycle && self.index == (self.vec.len() - 1) as isize) { 20 | return None; 21 | } 22 | 23 | self.index = (self.index + 1) % (self.vec.len() as isize); 24 | return Some(self.current()); 25 | } 26 | pub fn previous(&mut self) -> Option<&T> { 27 | if self.vec.is_empty() || (!self.cycle && (self.index == 0 || self.index == -1)) { 28 | if self.index == 0 { 29 | self.index = -1; 30 | } 31 | return None; 32 | } 33 | 34 | self.index = if self.index == 0 || self.index == -1 { 35 | (self.vec.len() - 1) as isize 36 | } else { 37 | (self.index - 1) % (self.vec.len() as isize) 38 | }; 39 | return Some(self.current()); 40 | } 41 | 42 | #[inline] 43 | pub fn first(&self) -> Option<&T> { 44 | self.vec.front() 45 | } 46 | // #[inline] 47 | // pub fn last<'a>(&'a self) -> Option<&'a T> { 48 | // self.vec.back() 49 | // } 50 | 51 | #[inline] 52 | pub fn current(&self) -> &T { 53 | if self.index == -1 { 54 | &self.vec[0] 55 | } else { 56 | &self.vec[self.index as usize] 57 | } 58 | } 59 | 60 | // --- --- --- --- --- --- 61 | 62 | // #[inline] 63 | // pub fn push_back(&mut self, element: T) { 64 | // self.vec.push_back(element); 65 | // } 66 | #[inline] 67 | pub fn push_front(&mut self, element: T) { 68 | self.vec.push_front(element); 69 | } 70 | // #[inline] 71 | // pub fn pop_back(&mut self) -> Option { 72 | // self.vec.pop_back() 73 | // } 74 | // #[inline] 75 | // pub fn pop_front(&mut self) -> Option { 76 | // self.vec.pop_front() 77 | // } 78 | 79 | // --- --- --- --- --- --- 80 | 81 | #[inline] 82 | pub fn reset_index(&mut self) { 83 | self.index = -1; 84 | } 85 | 86 | #[inline] 87 | pub fn set_content(&mut self, new_vec: Vec) { 88 | self.vec = VecDeque::from(new_vec); 89 | self.reset_index(); 90 | } 91 | 92 | #[inline] 93 | pub fn clear(&mut self) { 94 | self.vec.clear(); 95 | self.reset_index(); 96 | } 97 | 98 | #[inline] 99 | pub fn is_empty(&self) -> bool { 100 | self.vec.is_empty() 101 | } 102 | 103 | // #[inline] 104 | // pub fn len(&self) -> usize { 105 | // self.vec.len() 106 | // } 107 | } 108 | 109 | #[test] 110 | fn test() { 111 | let mut cycling_traverser = LoopTraverser::new(true); 112 | cycling_traverser.set_content(vec![1, 2, 3, 4]); 113 | 114 | assert_eq!(cycling_traverser.next(), Some(&1)); 115 | assert_eq!(cycling_traverser.next(), Some(&2)); 116 | assert_eq!(cycling_traverser.next(), Some(&3)); 117 | assert_eq!(cycling_traverser.next(), Some(&4)); 118 | assert_eq!(cycling_traverser.next(), Some(&1)); 119 | assert_eq!(cycling_traverser.next(), Some(&2)); 120 | assert_eq!(cycling_traverser.next(), Some(&3)); 121 | assert_eq!(cycling_traverser.next(), Some(&4)); 122 | 123 | assert_eq!(cycling_traverser.previous(), Some(&3)); 124 | assert_eq!(cycling_traverser.previous(), Some(&2)); 125 | assert_eq!(cycling_traverser.previous(), Some(&1)); 126 | assert_eq!(cycling_traverser.previous(), Some(&4)); 127 | assert_eq!(cycling_traverser.previous(), Some(&3)); 128 | assert_eq!(cycling_traverser.previous(), Some(&2)); 129 | assert_eq!(cycling_traverser.previous(), Some(&1)); 130 | assert_eq!(cycling_traverser.previous(), Some(&4)); 131 | 132 | // --- --- --- --- --- --- 133 | 134 | let mut uncycling_traverser = LoopTraverser::new(false); 135 | uncycling_traverser.set_content(vec![1, 2, 3, 4]); 136 | 137 | assert_eq!(uncycling_traverser.previous(), None); 138 | assert_eq!(uncycling_traverser.next(), Some(&1)); 139 | assert_eq!(uncycling_traverser.next(), Some(&2)); 140 | assert_eq!(uncycling_traverser.next(), Some(&3)); 141 | assert_eq!(uncycling_traverser.next(), Some(&4)); 142 | assert_eq!(uncycling_traverser.next(), None); 143 | assert_eq!(uncycling_traverser.previous(), Some(&3)); 144 | } 145 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod logger; 2 | mod loop_traverser; 3 | mod number_bit_count; 4 | 5 | pub mod cursor; 6 | pub mod terminal; 7 | 8 | pub use logger::log; 9 | pub use number_bit_count::number_bit_count; 10 | 11 | pub use cursor::Cursor; 12 | pub use loop_traverser::LoopTraverser; 13 | pub use terminal::Terminal; 14 | -------------------------------------------------------------------------------- /src/utils/number_bit_count.rs: -------------------------------------------------------------------------------- 1 | pub fn number_bit_count(mut num: usize) -> usize { 2 | if num == 0 { 3 | return 1; 4 | } 5 | 6 | let mut count = 0; 7 | while num > 0 { 8 | num /= 10; 9 | count += 1; 10 | } 11 | return count; 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/terminal.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write}; 2 | 3 | use crossterm::{ 4 | event::{self, Event, KeyEvent, KeyEventKind}, 5 | terminal::size, 6 | }; 7 | 8 | pub struct Terminal; 9 | 10 | impl Terminal { 11 | const BACKSPACE: &'static str = "\x1B[K"; 12 | 13 | pub fn width() -> usize { 14 | size().unwrap().0 as usize 15 | } 16 | 17 | pub fn height() -> usize { 18 | size().unwrap().1 as usize 19 | } 20 | 21 | pub fn flush() -> io::Result<()> { 22 | io::stdout().flush() 23 | } 24 | 25 | pub fn clear_after_cursor() -> io::Result<()> { 26 | print!("{}", Self::BACKSPACE); 27 | Self::flush() 28 | } 29 | 30 | pub fn get_key() -> Option { 31 | if let Ok(Event::Key(key)) = event::read() { 32 | if key.kind == KeyEventKind::Press { 33 | return Some(key); 34 | } 35 | } 36 | None 37 | } 38 | } 39 | --------------------------------------------------------------------------------