├── .DS_Store ├── .gitignore ├── CONTRIBUTING.MD ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.MD ├── assets └── screenshots │ ├── Editor_Initialized.png │ ├── Editor_Write_Mode.png │ └── Full_Functionality_Multiple_Files.png └── src ├── app ├── actions.rs ├── mod.rs ├── open_files_data.rs ├── state.rs └── ui.rs ├── inputs ├── events.rs ├── key.rs └── mod.rs ├── io ├── handler.rs └── mod.rs ├── lib.rs └── main.rs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhi13man/rust_text_editor/a42110f008830837fec68d604d635d47ea4b16b6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target -------------------------------------------------------------------------------- /CONTRIBUTING.MD: -------------------------------------------------------------------------------- 1 | # rust_text_editor 2 | 3 | [![License](https://img.shields.io/github/license/dhi13man/rust_text_editor)](https://github.com/Dhi13man/rust_text_editor/blob/main/LICENSE) 4 | [![Contributors](https://img.shields.io/github/contributors-anon/dhi13man/rust_text_editor?style=flat)](https://github.com/Dhi13man/rust_text_editor/graphs/contributors) 5 | [![GitHub forks](https://img.shields.io/github/forks/dhi13man/rust_text_editor?style=social)](https://github.com/Dhi13man/rust_text_editor/network/members) 6 | [![GitHub Repo stars](https://img.shields.io/github/stars/dhi13man/rust_text_editor?style=social)](https://github.com/Dhi13man/rust_text_editor) 7 | [![Last Commit](https://img.shields.io/github/last-commit/dhi13man/rust_text_editor)](https://github.com/Dhi13man/rust_text_editor/commits/main) 8 | 9 | Thank you for investing your time in contributing to this project! Although it is pretty bare bones, any contributions you make will brighten up my day. :) 10 | 11 | ## General Steps to Contribute 12 | 13 | 1. Ensure you have [Rust](https://www.rust-lang.org/tools/install) SDK installed. 14 | 15 | 2. Fork the [project repository](https://github.com/Dhi13man/rust_text_editor/). 16 | 17 | 3. Clone the forked repository by running `git clone `. 18 | 19 | 4. Navigate to your local repository by running `cd open_route_service`. 20 | 21 | 5. Pull the latest changes from upstream into your local repository by running `git pull`. 22 | 23 | 6. Create a new branch by running `git checkout -b `. 24 | 25 | 7. Make changes in your local repository to make the contribution you want. 26 | 27 | 8. Add relevant tests (if any) for the contribution you made. 28 | 29 | 9. Commit your changes and push them to your local repository by running `git commit -am "my-commit-message" && git push origin `. 30 | 31 | 10. Create a pull request on the original repository from your fork and wait for me to review (and hopefully merge) it. :) 32 | 33 | ### Recommended Development Workflow 34 | 35 | - Fork Project **->** Create new Branch 36 | - For each contribution in mind, 37 | - **->** Develop Data Models 38 | - **->** Develop API Bindings 39 | - **->** Test 40 | - **->** Ensure Documentation is sufficient 41 | - **->** Commit 42 | - Create Pull Request 43 | 44 | ## Issue Based Contributions 45 | 46 | ### Create a new issue 47 | 48 | If you spot a problem or bug with the package, search if an [issue](https://www.github.com/dhi13man/rust_text_editor/issues/) already exists. If a related issue doesn't exist, you can open a new issue using a relevant issue form. 49 | 50 | ### Solve an issue 51 | 52 | Scan through our existing [issues](https://www.github.com/dhi13man/rust_text_editor/issues/) to find one that interests you. You can narrow down the search using labels as filters. See Labels for more information. 53 | 54 | ## Overall Guidelines 55 | 56 | - Contributions are welcome on [GitHub](https://www.github.com/dhi13man/rust_text_editor/). Please ensure all the tests are running before pushing your changes. Write your own tests too! 57 | 58 | - File any [issues or feature requests here,](https://www.github.com/dhi13man/rust_text_editor/issues/) or help me resolve existing ones. :) 59 | -------------------------------------------------------------------------------- /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 = "android_system_properties" 7 | version = "0.1.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" 10 | dependencies = [ 11 | "libc", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "block" 28 | version = "0.1.6" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 31 | 32 | [[package]] 33 | name = "bumpalo" 34 | version = "3.10.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" 37 | 38 | [[package]] 39 | name = "byteorder" 40 | version = "1.4.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 43 | 44 | [[package]] 45 | name = "bytes" 46 | version = "1.2.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" 49 | 50 | [[package]] 51 | name = "cassowary" 52 | version = "0.3.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 55 | 56 | [[package]] 57 | name = "cc" 58 | version = "1.0.73" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" 61 | 62 | [[package]] 63 | name = "cfg-if" 64 | version = "1.0.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 67 | 68 | [[package]] 69 | name = "chrono" 70 | version = "0.4.21" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "3f725f340c3854e3cb3ab736dc21f0cca183303acea3b3ffec30f141503ac8eb" 73 | dependencies = [ 74 | "iana-time-zone", 75 | "js-sys", 76 | "num-integer", 77 | "num-traits", 78 | "time", 79 | "wasm-bindgen", 80 | "winapi", 81 | ] 82 | 83 | [[package]] 84 | name = "clipboard-win" 85 | version = "3.1.1" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342" 88 | dependencies = [ 89 | "lazy-bytes-cast", 90 | "winapi", 91 | ] 92 | 93 | [[package]] 94 | name = "copypasta" 95 | version = "0.8.1" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "d7216b5c1e9ad3867252505995b02d01c6fa7e6db0d8abd42634352ef377777e" 98 | dependencies = [ 99 | "clipboard-win", 100 | "objc", 101 | "objc-foundation", 102 | "objc_id", 103 | "smithay-clipboard", 104 | "x11-clipboard", 105 | ] 106 | 107 | [[package]] 108 | name = "core-foundation-sys" 109 | version = "0.8.3" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 112 | 113 | [[package]] 114 | name = "crossterm" 115 | version = "0.23.2" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" 118 | dependencies = [ 119 | "bitflags", 120 | "crossterm_winapi", 121 | "libc", 122 | "mio", 123 | "parking_lot", 124 | "signal-hook", 125 | "signal-hook-mio", 126 | "winapi", 127 | ] 128 | 129 | [[package]] 130 | name = "crossterm" 131 | version = "0.24.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "ab9f7409c70a38a56216480fba371ee460207dd8926ccf5b4160591759559170" 134 | dependencies = [ 135 | "bitflags", 136 | "crossterm_winapi", 137 | "libc", 138 | "mio", 139 | "parking_lot", 140 | "signal-hook", 141 | "signal-hook-mio", 142 | "winapi", 143 | ] 144 | 145 | [[package]] 146 | name = "crossterm_winapi" 147 | version = "0.9.0" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" 150 | dependencies = [ 151 | "winapi", 152 | ] 153 | 154 | [[package]] 155 | name = "dlib" 156 | version = "0.5.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794" 159 | dependencies = [ 160 | "libloading", 161 | ] 162 | 163 | [[package]] 164 | name = "downcast-rs" 165 | version = "1.2.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" 168 | 169 | [[package]] 170 | name = "eyre" 171 | version = "0.6.8" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" 174 | dependencies = [ 175 | "indenter", 176 | "once_cell", 177 | ] 178 | 179 | [[package]] 180 | name = "fxhash" 181 | version = "0.2.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 184 | dependencies = [ 185 | "byteorder", 186 | ] 187 | 188 | [[package]] 189 | name = "getrandom" 190 | version = "0.2.7" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" 193 | dependencies = [ 194 | "cfg-if", 195 | "libc", 196 | "wasi 0.11.0+wasi-snapshot-preview1", 197 | ] 198 | 199 | [[package]] 200 | name = "hermit-abi" 201 | version = "0.1.19" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 204 | dependencies = [ 205 | "libc", 206 | ] 207 | 208 | [[package]] 209 | name = "iana-time-zone" 210 | version = "0.1.44" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "808cf7d67cf4a22adc5be66e75ebdf769b3f2ea032041437a7061f97a63dad4b" 213 | dependencies = [ 214 | "android_system_properties", 215 | "core-foundation-sys", 216 | "js-sys", 217 | "wasm-bindgen", 218 | "winapi", 219 | ] 220 | 221 | [[package]] 222 | name = "indenter" 223 | version = "0.3.3" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 226 | 227 | [[package]] 228 | name = "js-sys" 229 | version = "0.3.59" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" 232 | dependencies = [ 233 | "wasm-bindgen", 234 | ] 235 | 236 | [[package]] 237 | name = "lazy-bytes-cast" 238 | version = "5.0.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b" 241 | 242 | [[package]] 243 | name = "lazy_static" 244 | version = "1.4.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 247 | 248 | [[package]] 249 | name = "libc" 250 | version = "0.2.131" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "04c3b4822ccebfa39c02fc03d1534441b22ead323fa0f48bb7ddd8e6ba076a40" 253 | 254 | [[package]] 255 | name = "libloading" 256 | version = "0.7.3" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" 259 | dependencies = [ 260 | "cfg-if", 261 | "winapi", 262 | ] 263 | 264 | [[package]] 265 | name = "lock_api" 266 | version = "0.4.7" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" 269 | dependencies = [ 270 | "autocfg", 271 | "scopeguard", 272 | ] 273 | 274 | [[package]] 275 | name = "log" 276 | version = "0.4.17" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 279 | dependencies = [ 280 | "cfg-if", 281 | ] 282 | 283 | [[package]] 284 | name = "malloc_buf" 285 | version = "0.0.6" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 288 | dependencies = [ 289 | "libc", 290 | ] 291 | 292 | [[package]] 293 | name = "memchr" 294 | version = "2.5.0" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 297 | 298 | [[package]] 299 | name = "memmap2" 300 | version = "0.5.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "8e2e4455be2010e8c5e77f0d10234b30f3a636a5305725609b5a71ad00d22577" 303 | dependencies = [ 304 | "libc", 305 | ] 306 | 307 | [[package]] 308 | name = "memoffset" 309 | version = "0.6.5" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 312 | dependencies = [ 313 | "autocfg", 314 | ] 315 | 316 | [[package]] 317 | name = "minimal-lexical" 318 | version = "0.2.1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 321 | 322 | [[package]] 323 | name = "mio" 324 | version = "0.8.4" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" 327 | dependencies = [ 328 | "libc", 329 | "log", 330 | "wasi 0.11.0+wasi-snapshot-preview1", 331 | "windows-sys", 332 | ] 333 | 334 | [[package]] 335 | name = "names" 336 | version = "0.14.0" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" 339 | dependencies = [ 340 | "rand", 341 | ] 342 | 343 | [[package]] 344 | name = "nix" 345 | version = "0.22.3" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" 348 | dependencies = [ 349 | "bitflags", 350 | "cc", 351 | "cfg-if", 352 | "libc", 353 | "memoffset", 354 | ] 355 | 356 | [[package]] 357 | name = "nix" 358 | version = "0.24.2" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" 361 | dependencies = [ 362 | "bitflags", 363 | "cfg-if", 364 | "libc", 365 | ] 366 | 367 | [[package]] 368 | name = "nom" 369 | version = "7.1.1" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" 372 | dependencies = [ 373 | "memchr", 374 | "minimal-lexical", 375 | ] 376 | 377 | [[package]] 378 | name = "num-integer" 379 | version = "0.1.45" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 382 | dependencies = [ 383 | "autocfg", 384 | "num-traits", 385 | ] 386 | 387 | [[package]] 388 | name = "num-traits" 389 | version = "0.2.15" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 392 | dependencies = [ 393 | "autocfg", 394 | ] 395 | 396 | [[package]] 397 | name = "num_cpus" 398 | version = "1.13.1" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" 401 | dependencies = [ 402 | "hermit-abi", 403 | "libc", 404 | ] 405 | 406 | [[package]] 407 | name = "objc" 408 | version = "0.2.7" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 411 | dependencies = [ 412 | "malloc_buf", 413 | ] 414 | 415 | [[package]] 416 | name = "objc-foundation" 417 | version = "0.1.1" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 420 | dependencies = [ 421 | "block", 422 | "objc", 423 | "objc_id", 424 | ] 425 | 426 | [[package]] 427 | name = "objc_id" 428 | version = "0.1.1" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 431 | dependencies = [ 432 | "objc", 433 | ] 434 | 435 | [[package]] 436 | name = "once_cell" 437 | version = "1.13.0" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" 440 | 441 | [[package]] 442 | name = "parking_lot" 443 | version = "0.12.1" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 446 | dependencies = [ 447 | "lock_api", 448 | "parking_lot_core", 449 | ] 450 | 451 | [[package]] 452 | name = "parking_lot_core" 453 | version = "0.9.3" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" 456 | dependencies = [ 457 | "cfg-if", 458 | "libc", 459 | "redox_syscall", 460 | "smallvec", 461 | "windows-sys", 462 | ] 463 | 464 | [[package]] 465 | name = "pin-project-lite" 466 | version = "0.2.9" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 469 | 470 | [[package]] 471 | name = "pkg-config" 472 | version = "0.3.25" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" 475 | 476 | [[package]] 477 | name = "ppv-lite86" 478 | version = "0.2.16" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 481 | 482 | [[package]] 483 | name = "proc-macro2" 484 | version = "1.0.43" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" 487 | dependencies = [ 488 | "unicode-ident", 489 | ] 490 | 491 | [[package]] 492 | name = "quick-xml" 493 | version = "0.22.0" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "8533f14c8382aaad0d592c812ac3b826162128b65662331e1127b45c3d18536b" 496 | dependencies = [ 497 | "memchr", 498 | ] 499 | 500 | [[package]] 501 | name = "quote" 502 | version = "1.0.21" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 505 | dependencies = [ 506 | "proc-macro2", 507 | ] 508 | 509 | [[package]] 510 | name = "rand" 511 | version = "0.8.5" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 514 | dependencies = [ 515 | "libc", 516 | "rand_chacha", 517 | "rand_core", 518 | ] 519 | 520 | [[package]] 521 | name = "rand_chacha" 522 | version = "0.3.1" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 525 | dependencies = [ 526 | "ppv-lite86", 527 | "rand_core", 528 | ] 529 | 530 | [[package]] 531 | name = "rand_core" 532 | version = "0.6.3" 533 | source = "registry+https://github.com/rust-lang/crates.io-index" 534 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 535 | dependencies = [ 536 | "getrandom", 537 | ] 538 | 539 | [[package]] 540 | name = "redox_syscall" 541 | version = "0.2.16" 542 | source = "registry+https://github.com/rust-lang/crates.io-index" 543 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 544 | dependencies = [ 545 | "bitflags", 546 | ] 547 | 548 | [[package]] 549 | name = "rust_text_editor" 550 | version = "0.1.2" 551 | dependencies = [ 552 | "copypasta", 553 | "crossterm 0.24.0", 554 | "eyre", 555 | "log", 556 | "names", 557 | "tokio", 558 | "tui", 559 | "tui-logger", 560 | ] 561 | 562 | [[package]] 563 | name = "scoped-tls" 564 | version = "1.0.0" 565 | source = "registry+https://github.com/rust-lang/crates.io-index" 566 | checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" 567 | 568 | [[package]] 569 | name = "scopeguard" 570 | version = "1.1.0" 571 | source = "registry+https://github.com/rust-lang/crates.io-index" 572 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 573 | 574 | [[package]] 575 | name = "signal-hook" 576 | version = "0.3.14" 577 | source = "registry+https://github.com/rust-lang/crates.io-index" 578 | checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" 579 | dependencies = [ 580 | "libc", 581 | "signal-hook-registry", 582 | ] 583 | 584 | [[package]] 585 | name = "signal-hook-mio" 586 | version = "0.2.3" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" 589 | dependencies = [ 590 | "libc", 591 | "mio", 592 | "signal-hook", 593 | ] 594 | 595 | [[package]] 596 | name = "signal-hook-registry" 597 | version = "1.4.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 600 | dependencies = [ 601 | "libc", 602 | ] 603 | 604 | [[package]] 605 | name = "slog" 606 | version = "2.7.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" 609 | 610 | [[package]] 611 | name = "smallvec" 612 | version = "1.9.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" 615 | 616 | [[package]] 617 | name = "smithay-client-toolkit" 618 | version = "0.16.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" 621 | dependencies = [ 622 | "bitflags", 623 | "dlib", 624 | "lazy_static", 625 | "log", 626 | "memmap2", 627 | "nix 0.24.2", 628 | "pkg-config", 629 | "wayland-client", 630 | "wayland-cursor", 631 | "wayland-protocols", 632 | ] 633 | 634 | [[package]] 635 | name = "smithay-clipboard" 636 | version = "0.6.6" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" 639 | dependencies = [ 640 | "smithay-client-toolkit", 641 | "wayland-client", 642 | ] 643 | 644 | [[package]] 645 | name = "socket2" 646 | version = "0.4.4" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" 649 | dependencies = [ 650 | "libc", 651 | "winapi", 652 | ] 653 | 654 | [[package]] 655 | name = "syn" 656 | version = "1.0.99" 657 | source = "registry+https://github.com/rust-lang/crates.io-index" 658 | checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" 659 | dependencies = [ 660 | "proc-macro2", 661 | "quote", 662 | "unicode-ident", 663 | ] 664 | 665 | [[package]] 666 | name = "time" 667 | version = "0.1.44" 668 | source = "registry+https://github.com/rust-lang/crates.io-index" 669 | checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" 670 | dependencies = [ 671 | "libc", 672 | "wasi 0.10.0+wasi-snapshot-preview1", 673 | "winapi", 674 | ] 675 | 676 | [[package]] 677 | name = "tokio" 678 | version = "1.20.1" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" 681 | dependencies = [ 682 | "autocfg", 683 | "bytes", 684 | "libc", 685 | "memchr", 686 | "mio", 687 | "num_cpus", 688 | "once_cell", 689 | "parking_lot", 690 | "pin-project-lite", 691 | "signal-hook-registry", 692 | "socket2", 693 | "tokio-macros", 694 | "winapi", 695 | ] 696 | 697 | [[package]] 698 | name = "tokio-macros" 699 | version = "1.8.0" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" 702 | dependencies = [ 703 | "proc-macro2", 704 | "quote", 705 | "syn", 706 | ] 707 | 708 | [[package]] 709 | name = "tui" 710 | version = "0.18.0" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "96fe69244ec2af261bced1d9046a6fee6c8c2a6b0228e59e5ba39bc8ba4ed729" 713 | dependencies = [ 714 | "bitflags", 715 | "cassowary", 716 | "crossterm 0.23.2", 717 | "unicode-segmentation", 718 | "unicode-width", 719 | ] 720 | 721 | [[package]] 722 | name = "tui-logger" 723 | version = "0.8.0" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "5c9564fd9c18a1f9a20fb8613494744778e357aa0cd5c7d85fdf699a4e5b4962" 726 | dependencies = [ 727 | "chrono", 728 | "fxhash", 729 | "lazy_static", 730 | "log", 731 | "parking_lot", 732 | "slog", 733 | "tui", 734 | ] 735 | 736 | [[package]] 737 | name = "unicode-ident" 738 | version = "1.0.3" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" 741 | 742 | [[package]] 743 | name = "unicode-segmentation" 744 | version = "1.9.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" 747 | 748 | [[package]] 749 | name = "unicode-width" 750 | version = "0.1.9" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" 753 | 754 | [[package]] 755 | name = "wasi" 756 | version = "0.10.0+wasi-snapshot-preview1" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" 759 | 760 | [[package]] 761 | name = "wasi" 762 | version = "0.11.0+wasi-snapshot-preview1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 765 | 766 | [[package]] 767 | name = "wasm-bindgen" 768 | version = "0.2.82" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" 771 | dependencies = [ 772 | "cfg-if", 773 | "wasm-bindgen-macro", 774 | ] 775 | 776 | [[package]] 777 | name = "wasm-bindgen-backend" 778 | version = "0.2.82" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" 781 | dependencies = [ 782 | "bumpalo", 783 | "log", 784 | "once_cell", 785 | "proc-macro2", 786 | "quote", 787 | "syn", 788 | "wasm-bindgen-shared", 789 | ] 790 | 791 | [[package]] 792 | name = "wasm-bindgen-macro" 793 | version = "0.2.82" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" 796 | dependencies = [ 797 | "quote", 798 | "wasm-bindgen-macro-support", 799 | ] 800 | 801 | [[package]] 802 | name = "wasm-bindgen-macro-support" 803 | version = "0.2.82" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" 806 | dependencies = [ 807 | "proc-macro2", 808 | "quote", 809 | "syn", 810 | "wasm-bindgen-backend", 811 | "wasm-bindgen-shared", 812 | ] 813 | 814 | [[package]] 815 | name = "wasm-bindgen-shared" 816 | version = "0.2.82" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" 819 | 820 | [[package]] 821 | name = "wayland-client" 822 | version = "0.29.4" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" 825 | dependencies = [ 826 | "bitflags", 827 | "downcast-rs", 828 | "libc", 829 | "nix 0.22.3", 830 | "scoped-tls", 831 | "wayland-commons", 832 | "wayland-scanner", 833 | "wayland-sys", 834 | ] 835 | 836 | [[package]] 837 | name = "wayland-commons" 838 | version = "0.29.4" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" 841 | dependencies = [ 842 | "nix 0.22.3", 843 | "once_cell", 844 | "smallvec", 845 | "wayland-sys", 846 | ] 847 | 848 | [[package]] 849 | name = "wayland-cursor" 850 | version = "0.29.4" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" 853 | dependencies = [ 854 | "nix 0.22.3", 855 | "wayland-client", 856 | "xcursor", 857 | ] 858 | 859 | [[package]] 860 | name = "wayland-protocols" 861 | version = "0.29.4" 862 | source = "registry+https://github.com/rust-lang/crates.io-index" 863 | checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" 864 | dependencies = [ 865 | "bitflags", 866 | "wayland-client", 867 | "wayland-commons", 868 | "wayland-scanner", 869 | ] 870 | 871 | [[package]] 872 | name = "wayland-scanner" 873 | version = "0.29.4" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" 876 | dependencies = [ 877 | "proc-macro2", 878 | "quote", 879 | "xml-rs", 880 | ] 881 | 882 | [[package]] 883 | name = "wayland-sys" 884 | version = "0.29.4" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" 887 | dependencies = [ 888 | "dlib", 889 | "lazy_static", 890 | "pkg-config", 891 | ] 892 | 893 | [[package]] 894 | name = "winapi" 895 | version = "0.3.9" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 898 | dependencies = [ 899 | "winapi-i686-pc-windows-gnu", 900 | "winapi-x86_64-pc-windows-gnu", 901 | ] 902 | 903 | [[package]] 904 | name = "winapi-i686-pc-windows-gnu" 905 | version = "0.4.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 908 | 909 | [[package]] 910 | name = "winapi-x86_64-pc-windows-gnu" 911 | version = "0.4.0" 912 | source = "registry+https://github.com/rust-lang/crates.io-index" 913 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 914 | 915 | [[package]] 916 | name = "windows-sys" 917 | version = "0.36.1" 918 | source = "registry+https://github.com/rust-lang/crates.io-index" 919 | checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" 920 | dependencies = [ 921 | "windows_aarch64_msvc", 922 | "windows_i686_gnu", 923 | "windows_i686_msvc", 924 | "windows_x86_64_gnu", 925 | "windows_x86_64_msvc", 926 | ] 927 | 928 | [[package]] 929 | name = "windows_aarch64_msvc" 930 | version = "0.36.1" 931 | source = "registry+https://github.com/rust-lang/crates.io-index" 932 | checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" 933 | 934 | [[package]] 935 | name = "windows_i686_gnu" 936 | version = "0.36.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" 939 | 940 | [[package]] 941 | name = "windows_i686_msvc" 942 | version = "0.36.1" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" 945 | 946 | [[package]] 947 | name = "windows_x86_64_gnu" 948 | version = "0.36.1" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" 951 | 952 | [[package]] 953 | name = "windows_x86_64_msvc" 954 | version = "0.36.1" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" 957 | 958 | [[package]] 959 | name = "x11-clipboard" 960 | version = "0.6.1" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "6a7468a5768fea473e6c8c0d4b60d6d7001a64acceaac267207ca0281e1337e8" 963 | dependencies = [ 964 | "xcb", 965 | ] 966 | 967 | [[package]] 968 | name = "xcb" 969 | version = "1.1.1" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "b127bf5bfe9dbb39118d6567e3773d4bbc795411a8e1ef7b7e056bccac0011a9" 972 | dependencies = [ 973 | "bitflags", 974 | "libc", 975 | "quick-xml", 976 | ] 977 | 978 | [[package]] 979 | name = "xcursor" 980 | version = "0.3.4" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" 983 | dependencies = [ 984 | "nom", 985 | ] 986 | 987 | [[package]] 988 | name = "xml-rs" 989 | version = "0.8.4" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" 992 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust_text_editor" 3 | version = "0.1.2" 4 | authors = ["dhi13man "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | repository = "https://github.com/Dhi13man/rust_text_editor" 8 | 9 | [dependencies] 10 | log = "0.4" 11 | 12 | copypasta = "0.8.1" 13 | 14 | names = { version = "0.14.0", default-features = false } 15 | 16 | tui-logger = "0.8.0" 17 | tui = "0.18.0" 18 | 19 | crossterm = "0.24.0" 20 | 21 | tokio = { version = "1", features = ["full"] } 22 | 23 | eyre = "0.6" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Dhiman Seal 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Rust Text Editor 2 | 3 | [![License](https://img.shields.io/github/license/dhi13man/rust_text_editor)](https://github.com/Dhi13man/rust_text_editor/blob/main/LICENSE) 4 | [![Contributors](https://img.shields.io/github/contributors-anon/dhi13man/rust_text_editor?style=flat)](https://github.com/Dhi13man/rust_text_editor/graphs/contributors) 5 | [![GitHub forks](https://img.shields.io/github/forks/dhi13man/rust_text_editor?style=social)](https://github.com/Dhi13man/rust_text_editor/network/members) 6 | [![GitHub Repo stars](https://img.shields.io/github/stars/dhi13man/rust_text_editor?style=social)](https://github.com/Dhi13man/rust_text_editor) 7 | [![Last Commit](https://img.shields.io/github/last-commit/dhi13man/rust_text_editor)](https://github.com/Dhi13man/rust_text_editor/commits/main) 8 | 9 | Creation of a Rust-backed text editor that supports certain Vim like commands. Built in 6-ish as a part of a Hackathon. 10 | 11 | ## Installation and Contribution 12 | 13 | 1. Ensure you have [Rust](https://www.rust-lang.org/tools/install) SDK installed. 14 | 15 | 2. Clone this repository and CD into it. 16 | 17 | 3. Please view the [Contrbution Guidelines](https://raw.githubusercontent.com/Dhi13man/rust_text_editor/master/CONTRIBUTING.MD) here to know how you can contribute to this. 18 | 19 | ## Usage 20 | 21 | 1. Clone the repository and CD into it. 22 | 23 | 2. Execute `cargo run`. 24 | 25 | 3. Follow the commands in the Help Section. To open files, copy their path into your clipboard and hit `Ctrl + O`. 26 | 27 | ## Features 28 | 29 | - Basic TUI Setup 30 | - Proper Logs at all times 31 | - User Friendly Help Menu 32 | - Create Random Files 33 | - Edit Files, Save Files, Open Multiple Files from the file paths saved in your Clipboard 34 | - Word Wrap and Responsive UI 35 | - Scrollable UI 36 | 37 | ## Inital Bugs and Open Issues (Feel Free to Contribute Fixes) 38 | 39 | - [ ] Unoptimized: The Editor has virtually no optimization 40 | - [ ] No Inuitive File Opener System: It can only open files when you copy the path to the files in your clipboard and hit the relevant shortcut 41 | - [ ] A Mess: Code works, but is not organised too well 42 | - [ ] Frontend: The UI could probably be improved 43 | 44 | ## Acknowledgement 45 | 46 | ### ilaborie/plop-tui 47 | 48 | This project is built on top of [plop-tui](https://github.com/ilaborie/plop-tui) to implement generic features of a CLI based Code editor. 49 | 50 | ### Dependencies 51 | 52 | The project utilizes various open-source Cargo Crate dependencies to work. Big thanks to all the creators. 53 | 54 | ```toml 55 | log = "0.4" 56 | 57 | copypasta = "0.8.1" 58 | 59 | names = { version = "0.14.0", default-features = false } 60 | 61 | tui-logger = "0.8.0" 62 | tui = "0.18.0" 63 | 64 | crossterm = "0.24.0" 65 | 66 | tokio = { version = "1", features = ["full"] } 67 | 68 | eyre = "0.6" 69 | ``` 70 | 71 | ## Screenshots 72 | 73 | | Editor Initialized | 74 | | :---: | 75 | | ![Editor Initialized](https://raw.githubusercontent.com/Dhi13man/rust_text_editor/master/assets/screenshots/Editor_Initialized.png) | 76 | 77 | | Editor Write Mode | 78 | | :---: | 79 | | ![Editor Write Mode](https://raw.githubusercontent.com/Dhi13man/rust_text_editor/master/assets/screenshots/Editor_Write_Mode.png) | 80 | 81 | | Full Functionality With Multiple Files | 82 | | :---: | 83 | | ![Full Functionality With Multiple Files](https://raw.githubusercontent.com/Dhi13man/rust_text_editor/master/assets/screenshots/Full_Functionality_Multiple_Files.png) | 84 | -------------------------------------------------------------------------------- /assets/screenshots/Editor_Initialized.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhi13man/rust_text_editor/a42110f008830837fec68d604d635d47ea4b16b6/assets/screenshots/Editor_Initialized.png -------------------------------------------------------------------------------- /assets/screenshots/Editor_Write_Mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhi13man/rust_text_editor/a42110f008830837fec68d604d635d47ea4b16b6/assets/screenshots/Editor_Write_Mode.png -------------------------------------------------------------------------------- /assets/screenshots/Full_Functionality_Multiple_Files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dhi13man/rust_text_editor/a42110f008830837fec68d604d635d47ea4b16b6/assets/screenshots/Full_Functionality_Multiple_Files.png -------------------------------------------------------------------------------- /src/app/actions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::{self, Display}; 3 | use std::slice::Iter; 4 | 5 | use crate::inputs::key::Key; 6 | 7 | /// We define all available action 8 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 9 | pub enum Action { 10 | Quit, 11 | BeginWriteMode, 12 | EndWriteMode, 13 | OpenFile, 14 | SaveFile, 15 | NextFile, 16 | PreviousFile, 17 | CloseFile, 18 | ScrollDown, 19 | ScrollUp, 20 | ScrollLeft, 21 | ScrollRight, 22 | } 23 | 24 | impl Action { 25 | /// All available actions 26 | pub fn iterator() -> Iter<'static, Action> { 27 | static ACTIONS: [Action; 12] = [ 28 | Action::Quit, 29 | Action::BeginWriteMode, 30 | Action::EndWriteMode, 31 | Action::OpenFile, 32 | Action::SaveFile, 33 | Action::NextFile, 34 | Action::PreviousFile, 35 | Action::CloseFile, 36 | Action::ScrollDown, 37 | Action::ScrollUp, 38 | Action::ScrollLeft, 39 | Action::ScrollRight, 40 | ]; 41 | ACTIONS.iter() 42 | } 43 | 44 | pub fn values() -> Vec { 45 | Action::iterator().cloned().collect() 46 | } 47 | 48 | /// List of key associated to action 49 | pub fn keys(&self) -> &[Key] { 50 | match self { 51 | Action::Quit => &[Key::Char('q')], 52 | Action::BeginWriteMode => &[Key::Char('w')], 53 | Action::EndWriteMode => &[Key::Ctrl('w')], 54 | Action::OpenFile => &[Key::Ctrl('o')], 55 | Action::SaveFile => &[Key::Ctrl('s')], 56 | Action::NextFile => &[Key::Char('n')], 57 | Action::PreviousFile => &[Key::Char('p')], 58 | Action::CloseFile => &[Key::Ctrl('c')], 59 | Action::ScrollDown => &[Key::Down], 60 | Action::ScrollUp => &[Key::Up], 61 | Action::ScrollLeft => &[Key::Left], 62 | Action::ScrollRight => &[Key::Right], 63 | } 64 | } 65 | } 66 | 67 | /// Could display a user friendly short description of action 68 | impl Display for Action { 69 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 70 | let str = match self { 71 | Action::Quit => "Quit", 72 | Action::BeginWriteMode => "Begin Write Mode", 73 | Action::EndWriteMode => "End Write Mode", 74 | Action::OpenFile => "Open Copied Path", 75 | Action::SaveFile => "Save File", 76 | Action::NextFile => "Next File", 77 | Action::PreviousFile => "Previous File", 78 | Action::CloseFile => "Close File", 79 | Action::ScrollDown => "Scroll Down", 80 | Action::ScrollUp => "Scroll Up", 81 | Action::ScrollLeft => "Scroll Left", 82 | Action::ScrollRight => "Scroll Right", 83 | }; 84 | write!(f, "{}", str) 85 | } 86 | } 87 | 88 | /// The application should have some contextual actions. 89 | #[derive(Default, Debug, Clone)] 90 | pub struct Actions(Vec); 91 | 92 | impl Actions { 93 | /// Given a key, find the corresponding action 94 | pub fn find(&self, key: Key) -> Option<&Action> { 95 | Action::iterator() 96 | .filter(|action| self.0.contains(action)) 97 | .find(|action| action.keys().contains(&key)) 98 | } 99 | 100 | /// Get contextual actions. 101 | /// (just for building a help view) 102 | pub fn actions(&self) -> &[Action] { 103 | self.0.as_slice() 104 | } 105 | } 106 | 107 | impl From> for Actions { 108 | /// Build contextual action 109 | /// 110 | /// # Panics 111 | /// 112 | /// If two actions have same key 113 | fn from(actions: Vec) -> Self { 114 | // Check key unicity 115 | let mut map: HashMap> = HashMap::new(); 116 | for action in actions.iter() { 117 | for key in action.keys().iter() { 118 | match map.get_mut(key) { 119 | Some(vec) => vec.push(*action), 120 | None => { 121 | map.insert(*key, vec![*action]); 122 | } 123 | } 124 | } 125 | } 126 | let errors = map 127 | .iter() 128 | .filter(|(_, actions)| actions.len() > 1) // at least two actions share same shortcut 129 | .map(|(key, actions)| { 130 | let actions = actions 131 | .iter() 132 | .map(Action::to_string) 133 | .collect::>() 134 | .join(", "); 135 | format!("Conflict key {} with actions {}", key, actions) 136 | }) 137 | .collect::>(); 138 | if !errors.is_empty() { 139 | panic!("{}", errors.join("; ")) 140 | } 141 | 142 | // Ok, we can create contextual actions 143 | Self(actions) 144 | } 145 | } 146 | 147 | #[cfg(test)] 148 | mod tests { 149 | use super::*; 150 | 151 | #[test] 152 | fn should_find_action_by_key() { 153 | let actions: Actions = vec![Action::Quit, Action::BeginWriteMode, Action::EndWriteMode].into(); 154 | let result = actions.find(Key::Ctrl('c')); 155 | assert_eq!(result, Some(&Action::Quit)); 156 | } 157 | 158 | #[test] 159 | fn should_find_action_by_key_not_found() { 160 | let actions: Actions = vec![Action::Quit, Action::BeginWriteMode, Action::EndWriteMode].into(); 161 | let result = actions.find(Key::Alt('w')); 162 | assert_eq!(result, None); 163 | } 164 | 165 | #[test] 166 | fn should_create_actions_from_vec() { 167 | let _actions: Actions = vec![ 168 | Action::Quit, 169 | Action::BeginWriteMode, 170 | Action::EndWriteMode, 171 | Action::OpenFile, 172 | Action::SaveFile, 173 | Action::NextFile, 174 | Action::PreviousFile, 175 | Action::CloseFile, 176 | Action::ScrollDown, 177 | Action::ScrollUp, 178 | Action::ScrollLeft, 179 | Action::ScrollRight, 180 | ] 181 | .into(); 182 | } 183 | 184 | #[test] 185 | #[should_panic] 186 | fn should_panic_when_create_actions_conflict_key() { 187 | let _actions: Actions = vec![ 188 | Action::Quit, 189 | Action::BeginWriteMode, 190 | Action::EndWriteMode, 191 | Action::OpenFile, 192 | Action::SaveFile, 193 | Action::NextFile, 194 | Action::PreviousFile, 195 | Action::CloseFile, 196 | Action::ScrollDown, 197 | Action::ScrollUp, 198 | Action::ScrollLeft, 199 | Action::ScrollRight, 200 | ] 201 | .into(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/app/mod.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error, warn}; 2 | 3 | use self::actions::Actions; 4 | use self::open_files_data::OpenFilesData; 5 | use self::state::AppState; 6 | use crate::app::actions::Action; 7 | use crate::inputs::key::Key; 8 | use crate::io::IoEvent; 9 | 10 | pub mod open_files_data; 11 | pub mod actions; 12 | pub mod state; 13 | pub mod ui; 14 | 15 | #[derive(Debug, PartialEq, Eq)] 16 | pub enum AppReturn { 17 | Exit, 18 | Continue, 19 | } 20 | 21 | /// The main application, containing the state 22 | pub struct App { 23 | /// We could dispatch an IO event 24 | io_tx: tokio::sync::mpsc::Sender, 25 | /// Contextual actions 26 | actions: Actions, 27 | /// State 28 | is_loading: bool, 29 | state: AppState, 30 | } 31 | 32 | impl App { 33 | pub fn new(io_tx: tokio::sync::mpsc::Sender) -> Self { 34 | let actions = vec![Action::Quit].into(); 35 | let is_loading = false; 36 | let state = AppState::default(); 37 | 38 | Self { 39 | io_tx, 40 | actions, 41 | is_loading, 42 | state, 43 | } 44 | } 45 | 46 | /// Handle a user action 47 | pub async fn do_action(&mut self, key: Key) -> AppReturn { 48 | if let Some(value) = self.attempt_write(key) { 49 | value 50 | } else if let Some(action) = self.actions.find(key) { 51 | debug!("Run action [{:?}]", action); 52 | match action { 53 | Action::Quit => AppReturn::Exit, 54 | // Write o clock 55 | Action::BeginWriteMode => { 56 | self.dispatch(IoEvent::ToggleWriteMode(true)).await; 57 | AppReturn::Continue 58 | } 59 | // No more writing 60 | Action::EndWriteMode => { 61 | self.dispatch(IoEvent::ToggleWriteMode(false)).await; 62 | AppReturn::Continue 63 | }, 64 | // Open file 65 | Action::OpenFile => { 66 | self.dispatch(IoEvent::OpenFile).await; 67 | AppReturn::Continue 68 | }, 69 | // Save file 70 | Action::SaveFile => { 71 | self.dispatch(IoEvent::SaveFile).await; 72 | AppReturn::Continue 73 | }, 74 | // Next file 75 | Action::NextFile => { 76 | self.dispatch(IoEvent::NextFile).await; 77 | AppReturn::Continue 78 | }, 79 | // Previous file 80 | Action::PreviousFile => { 81 | self.dispatch(IoEvent::PreviousFile).await; 82 | AppReturn::Continue 83 | }, 84 | // Close file 85 | Action::CloseFile => { 86 | self.dispatch(IoEvent::CloseFile).await; 87 | AppReturn::Continue 88 | }, 89 | // Scroll down 90 | Action::ScrollDown => { 91 | self.dispatch(IoEvent::ScrollDown).await; 92 | AppReturn::Continue 93 | }, 94 | // Scroll up 95 | Action::ScrollUp => { 96 | self.dispatch(IoEvent::ScrollUp).await; 97 | AppReturn::Continue 98 | }, 99 | // Scroll left 100 | Action::ScrollLeft => { 101 | self.dispatch(IoEvent::ScrollLeft).await; 102 | AppReturn::Continue 103 | }, 104 | // Scroll right 105 | Action::ScrollRight => { 106 | self.dispatch(IoEvent::ScrollRight).await; 107 | AppReturn::Continue 108 | }, 109 | } 110 | } else { 111 | warn!("No action accociated to {}", key); 112 | AppReturn::Continue 113 | } 114 | } 115 | 116 | fn attempt_write(&mut self, key: Key) -> Option { 117 | if self.state.is_write_mode() { 118 | let mut curr_text = self.state.get_text(); 119 | match key { 120 | Key::Backspace => { 121 | curr_text.pop(); 122 | self.state.replace_text(&curr_text); 123 | Some(AppReturn::Continue) 124 | }, 125 | 126 | Key::Enter => { 127 | curr_text.push('\n'); 128 | self.state.replace_text(&curr_text); 129 | Some(AppReturn::Continue) 130 | }, 131 | 132 | Key::Space => { 133 | curr_text.push(' '); 134 | self.state.replace_text(&curr_text); 135 | Some(AppReturn::Continue) 136 | }, 137 | 138 | Key::Char(key_char) => { 139 | curr_text.push(key_char); 140 | self.state.replace_text(&curr_text); 141 | Some(AppReturn::Continue) 142 | }, 143 | 144 | _ => None, 145 | } 146 | } else { 147 | None 148 | } 149 | } 150 | 151 | /// Send a network event to the IO thread 152 | pub async fn dispatch(&mut self, action: IoEvent) { 153 | // `is_loading` will be set to false again after the async action has finished in io/handler.rs 154 | self.is_loading = true; 155 | if let Err(e) = self.io_tx.send(action).await { 156 | self.is_loading = false; 157 | error!("Error from dispatch {}", e); 158 | }; 159 | } 160 | 161 | pub fn actions(&self) -> &Actions { 162 | &self.actions 163 | } 164 | 165 | pub fn state(&self) -> &AppState { 166 | &self.state 167 | } 168 | 169 | pub fn open_files_data_mut(&mut self) -> &mut OpenFilesData { 170 | if let AppState::Initialized { files_data, .. } = &mut self.state { 171 | files_data 172 | } else { 173 | panic!("AppState is not Initialized"); 174 | } 175 | } 176 | 177 | pub fn is_loading(&self) -> bool { 178 | self.is_loading 179 | } 180 | 181 | pub fn initialized(&mut self) { 182 | // Update contextual actions 183 | self.actions = Action::values().into(); 184 | self.state = AppState::initialized() 185 | } 186 | 187 | pub fn loaded(&mut self) { 188 | self.is_loading = false; 189 | } 190 | 191 | pub fn toggle_write_mode(&mut self, new_write_mode: bool) { 192 | self.state.toggle_write_mode(new_write_mode); 193 | } 194 | 195 | pub fn scroll_horizontal(&mut self, delta: i32) -> Result<(), String> { 196 | self.state.scroll_horizontal(delta) 197 | } 198 | 199 | pub fn scroll_vertical(&mut self, delta: i32) -> Result<(), String> { 200 | self.state.scroll_vertical(delta) 201 | } 202 | 203 | pub fn reset_scroll(&mut self) { 204 | self.state.reset_scroll(); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/app/open_files_data.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | #[derive(Clone)] 4 | pub struct OpenFilesData { 5 | file_paths: Vec, 6 | file_contents: Vec, 7 | currently_selected_file_index: usize, 8 | } 9 | 10 | impl OpenFilesData { 11 | pub fn new() -> Self { 12 | Self { 13 | file_paths: vec![], 14 | file_contents: vec![], 15 | currently_selected_file_index: 0, 16 | } 17 | } 18 | 19 | pub fn open_file(&mut self, file_path: &str) -> Result<(), String> { 20 | if self.file_paths.contains(&file_path.to_owned()) { 21 | return Err(format!("File {} already opened", file_path)); 22 | } else { 23 | if !Path::new(file_path).exists() { 24 | Err(format!("File {} does not exist", file_path)) 25 | } else { 26 | let file_content = std::fs::read_to_string(file_path); 27 | if let Ok(file_content) = file_content { 28 | self.file_paths.push(file_path.to_owned()); 29 | self.file_contents.push(file_content); 30 | self.currently_selected_file_index = self.file_paths.len() - 1; 31 | Ok(()) 32 | } else { 33 | Err(format!("Error while reading file {}", file_path)) 34 | } 35 | } 36 | } 37 | } 38 | 39 | pub fn close_file(&mut self) -> Result<(), String> { 40 | if self.file_paths.is_empty() { 41 | return Err("No file to close".to_owned()); 42 | } else { 43 | self.file_paths.remove(self.currently_selected_file_index); 44 | self.file_contents.remove(self.currently_selected_file_index); 45 | self.select_previous_file(); 46 | Ok(()) 47 | } 48 | } 49 | 50 | pub fn get_open_file_paths(&self) -> &Vec { 51 | &self.file_paths 52 | } 53 | 54 | pub fn get_open_file_names(&self) -> Vec { 55 | self.file_paths.iter() 56 | .map(|file_path| file_path.split("/").last().unwrap().to_owned()) 57 | .collect::>() 58 | } 59 | 60 | pub fn get_open_file_contents(&self) -> &Vec { 61 | &self.file_contents 62 | } 63 | 64 | pub fn get_currently_selected_file_content(&self) -> String { 65 | if self.currently_selected_file_index < self.file_contents.len() { 66 | self.file_contents[self.currently_selected_file_index].clone() 67 | } else { 68 | "".to_owned() 69 | } 70 | } 71 | 72 | pub fn replace_currently_selected_file_content(&mut self, new_content: &str) { 73 | if self.currently_selected_file_index < self.file_contents.len() { 74 | self.file_contents[self.currently_selected_file_index] = new_content.to_owned(); 75 | } else { 76 | self.file_contents.push(new_content.to_owned()); 77 | } 78 | } 79 | 80 | pub fn get_currently_selected_file_path(&mut self) -> String { 81 | if self.currently_selected_file_index < self.file_paths.len() { 82 | self.file_paths[self.currently_selected_file_index].clone() 83 | } else { 84 | let random_file_name: String = names::Generator::default().next().unwrap(); 85 | self.file_paths.push(random_file_name.clone()); 86 | random_file_name 87 | } 88 | } 89 | 90 | pub fn get_currently_selected_file_name(&mut self) -> String { 91 | let path = self.get_currently_selected_file_path(); 92 | path.split("/").last().unwrap().to_owned() 93 | } 94 | 95 | pub fn select_next_file(&mut self) { 96 | if self.file_contents.len() > 0 { 97 | self.currently_selected_file_index = (self.currently_selected_file_index + 1) % self.file_contents.len(); 98 | } else { 99 | self.currently_selected_file_index = 0; 100 | } 101 | } 102 | 103 | pub fn select_previous_file(&mut self) { 104 | if self.file_contents.len() > 0 { 105 | self.currently_selected_file_index = (self.currently_selected_file_index + self.file_paths.len() - 1) % self.file_paths.len(); 106 | } else { 107 | self.currently_selected_file_index = 0; 108 | } 109 | } 110 | 111 | pub fn save_file(&mut self) -> Result<(), String> { 112 | let file_content = self.file_contents[self.currently_selected_file_index].clone(); 113 | if self.currently_selected_file_index < self.file_paths.len() { 114 | let file_path = self.file_paths[self.currently_selected_file_index].clone(); 115 | std::fs::write(&file_path, file_content).map_err(|e| format!("Error while writing file {}: {}", file_path, e)) 116 | } else { 117 | let random_file_name: String = names::Generator::default().next().unwrap(); 118 | // Take file path input from user 119 | self.file_paths.push(random_file_name.clone()); 120 | std::fs::write(&random_file_name, file_content).map_err(|e| format!("Error while writing file {}: {}", random_file_name, e)) 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/app/state.rs: -------------------------------------------------------------------------------- 1 | use super::open_files_data::OpenFilesData; 2 | 3 | #[derive(Clone)] 4 | pub enum AppState { 5 | Init, 6 | Initialized { 7 | write_mode: bool, 8 | scroll_offset: (u16, u16), 9 | files_data: OpenFilesData, 10 | }, 11 | } 12 | 13 | impl AppState { 14 | pub fn initialized() -> Self { 15 | Self::Initialized { 16 | write_mode: false, 17 | scroll_offset: (0, 0), 18 | files_data: OpenFilesData::new(), 19 | } 20 | } 21 | 22 | pub fn is_initialized(&self) -> bool { 23 | matches!(self, &Self::Initialized { .. }) 24 | } 25 | 26 | pub fn is_write_mode(&self) -> bool { 27 | matches!(self, &Self::Initialized { write_mode: true, .. }) 28 | } 29 | 30 | pub fn toggle_write_mode(&mut self, new_write_mode: bool) { 31 | if let Self::Initialized { write_mode, .. } = self { 32 | *write_mode = new_write_mode; 33 | } 34 | } 35 | 36 | pub fn get_text(&self) -> String { 37 | match self { 38 | Self::Initialized { files_data, write_mode, .. } => { 39 | if !*write_mode { 40 | let mut out: String = files_data.get_currently_selected_file_content(); 41 | out.push_str(" (input mode)"); 42 | out 43 | } else { 44 | files_data.get_currently_selected_file_content() 45 | } 46 | }, 47 | _ => "".to_owned(), 48 | } 49 | } 50 | 51 | pub fn replace_text(&mut self, new_text: &str) { 52 | if let Self::Initialized { files_data, .. } = self { 53 | files_data.replace_currently_selected_file_content(new_text); 54 | } 55 | } 56 | 57 | pub fn get_path(&mut self) -> String { 58 | match self { 59 | Self::Initialized { files_data, .. } => files_data.get_currently_selected_file_path(), 60 | _ => "..loading..".to_owned(), 61 | } 62 | } 63 | 64 | pub fn get_all_open_file_names(&self) -> String { 65 | match self { 66 | Self::Initialized { files_data, .. } => files_data.get_open_file_names().join(", "), 67 | _ => "..loading..".to_owned(), 68 | } 69 | } 70 | 71 | pub fn get_scroll_offset(&self) -> &(u16, u16) { 72 | match self { 73 | Self::Initialized { scroll_offset, .. } => scroll_offset, 74 | _ => &(0, 0), 75 | } 76 | } 77 | 78 | pub fn scroll_vertical(&mut self, delta: i32) -> Result<(), String> { 79 | let text = self.get_text(); 80 | if let Self::Initialized { scroll_offset, .. } = self { 81 | let (x, y) = scroll_offset; 82 | if delta > 0 { 83 | if *y < text.lines().count() as u16 { 84 | *scroll_offset = (*x, *y + delta as u16); 85 | Ok(()) 86 | } else { 87 | Err("Cannot scroll past end of file".to_owned()) 88 | } 89 | } else if delta < 0 { 90 | if *y > 0 { 91 | *scroll_offset = (*x, (*y as i32 + delta).max(0) as u16); 92 | Ok(()) 93 | } else { 94 | Err("Cannot scroll past start of file".to_owned()) 95 | } 96 | } else { 97 | Ok(()) 98 | } 99 | } else { 100 | Err("Not initialized".to_owned()) 101 | } 102 | } 103 | 104 | pub fn scroll_horizontal(&mut self, delta: i32) -> Result<(), String> { 105 | let text = self.get_text(); 106 | if let Self::Initialized { scroll_offset, .. } = self { 107 | let (x, y) = scroll_offset; 108 | if delta > 0 { 109 | if *x < text.lines().nth(*y as usize).unwrap().len() as u16 { 110 | *scroll_offset = (*x + delta as u16, *y); 111 | Ok(()) 112 | } else { 113 | Err("Cannot scroll past end of line".to_owned()) 114 | } 115 | } else if delta < 0 { 116 | if *x > 0 { 117 | *scroll_offset = ((*x as i32 + delta).max(0) as u16, *y); 118 | Ok(()) 119 | } else { 120 | Err("Cannot scroll past start of line".to_owned()) 121 | } 122 | } else { 123 | Ok(()) 124 | } 125 | } else { 126 | Err("Not initialized".to_owned()) 127 | } 128 | } 129 | 130 | pub fn reset_scroll(&mut self) { 131 | if let Self::Initialized { scroll_offset, .. } = self { 132 | *scroll_offset = (0, 0); 133 | } 134 | } 135 | } 136 | 137 | impl Default for AppState { 138 | fn default() -> Self { 139 | Self::Init 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/app/ui.rs: -------------------------------------------------------------------------------- 1 | use tui::backend::Backend; 2 | use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; 3 | use tui::style::{Color, Style}; 4 | use tui::text::{Span, Spans}; 5 | use tui::widgets::{Block, BorderType, Borders, Cell, Paragraph, Row, Table, Wrap}; 6 | use tui::{Frame}; 7 | use tui_logger::TuiLoggerWidget; 8 | 9 | use super::actions::Actions; 10 | use super::state::AppState; 11 | use crate::app::App; 12 | 13 | pub fn draw(rect: &mut Frame, app: &mut App) 14 | where 15 | B: Backend, 16 | { 17 | let size: Rect = rect.size(); 18 | check_size(&size); 19 | 20 | // Vertical layout 21 | let chunks: Vec = Layout::default() 22 | .direction(Direction::Vertical) 23 | .constraints( 24 | [ 25 | Constraint::Length(3), 26 | Constraint::Min(10), 27 | Constraint::Length(12), 28 | ] 29 | .as_ref(), 30 | ) 31 | .split(size); 32 | 33 | // Title 34 | let title: Paragraph = draw_title(&mut app.state); 35 | rect.render_widget(title, chunks[0]); 36 | 37 | // Body & Help 38 | let body_chunks: Vec = Layout::default() 39 | .direction(Direction::Horizontal) 40 | .constraints([Constraint::Min(20), Constraint::Length(32)].as_ref()) 41 | .split(chunks[1]); 42 | 43 | let body: Paragraph = draw_body(app.is_loading(), app.state()); 44 | rect.render_widget(body, body_chunks[0]); 45 | 46 | let help: Table = draw_help(app.actions()); 47 | rect.render_widget(help, body_chunks[1]); 48 | 49 | // Logs 50 | let logs: TuiLoggerWidget = draw_logs(); 51 | rect.render_widget(logs, chunks[2]); 52 | } 53 | 54 | fn draw_title<'a>(state: &mut AppState) -> Paragraph<'a> { 55 | let mut title: String = "Rust Text Editor: ".to_owned(); 56 | title.push_str(&state.get_path()); 57 | title.push_str(" ["); 58 | title.push_str(&state.get_all_open_file_names()); 59 | title.push_str("]"); 60 | Paragraph::new(title) 61 | .style(Style::default().fg(Color::LightCyan)) 62 | .alignment(Alignment::Center) 63 | .block( 64 | Block::default() 65 | .borders(Borders::ALL) 66 | .style(Style::default().fg(Color::White)) 67 | .border_type(BorderType::Plain), 68 | ) 69 | } 70 | 71 | fn check_size(rect: &Rect) { 72 | if rect.width < 52 { 73 | panic!("Require width >= 52, (got {})", rect.width); 74 | } 75 | if rect.height < 28 { 76 | panic!("Require height >= 28, (got {})", rect.height); 77 | } 78 | } 79 | 80 | fn draw_body<'a>(loading: bool, state: &AppState) -> Paragraph<'a> { 81 | let initialized_text = if !loading && state.is_initialized() { 82 | state.get_text().to_owned() 83 | } else { 84 | "..loading".to_owned() 85 | }; 86 | 87 | // Split text into lines 88 | let text: Vec = initialized_text.lines() 89 | .map(|line| Spans::from(Span::raw(line.to_owned()))) 90 | .collect(); 91 | Paragraph::new(text) 92 | .style(Style::default().fg(Color::LightCyan)) 93 | .alignment(Alignment::Left) 94 | .block( 95 | Block::default() 96 | .borders(Borders::ALL) 97 | .style(Style::default().fg(Color::White)) 98 | .border_type(BorderType::Plain), 99 | ).wrap(Wrap { trim: true } ) 100 | .scroll(state.get_scroll_offset().clone()) 101 | } 102 | 103 | fn draw_help(actions: &Actions) -> Table { 104 | let key_style = Style::default().fg(Color::LightCyan); 105 | let help_style = Style::default().fg(Color::Gray); 106 | 107 | let mut rows = vec![]; 108 | for action in actions.actions().iter() { 109 | let mut first = true; 110 | for key in action.keys() { 111 | let help = if first { 112 | first = false; 113 | action.to_string() 114 | } else { 115 | String::from("") 116 | }; 117 | let row = Row::new(vec![ 118 | Cell::from(Span::styled(key.to_string(), key_style)), 119 | Cell::from(Span::styled(help, help_style)), 120 | ]); 121 | rows.push(row); 122 | } 123 | } 124 | 125 | Table::new(rows) 126 | .block( 127 | Block::default() 128 | .borders(Borders::ALL) 129 | .border_type(BorderType::Plain) 130 | .title("Help"), 131 | ) 132 | .widths(&[Constraint::Length(11), Constraint::Min(20)]) 133 | .column_spacing(1) 134 | } 135 | 136 | fn draw_logs<'a>() -> TuiLoggerWidget<'a> { 137 | TuiLoggerWidget::default() 138 | .style_error(Style::default().fg(Color::Red)) 139 | .style_debug(Style::default().fg(Color::Green)) 140 | .style_warn(Style::default().fg(Color::Yellow)) 141 | .style_trace(Style::default().fg(Color::Gray)) 142 | .style_info(Style::default().fg(Color::Blue)) 143 | .block( 144 | Block::default() 145 | .title("Logs") 146 | .border_style(Style::default().fg(Color::White).bg(Color::Black)) 147 | .borders(Borders::ALL), 148 | ) 149 | .style(Style::default().fg(Color::White).bg(Color::Black)) 150 | } 151 | -------------------------------------------------------------------------------- /src/inputs/events.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use log::error; 6 | 7 | use super::key::Key; 8 | use super::InputEvent; 9 | 10 | /// A small event handler that wrap crossterm input and tick event. Each event 11 | /// type is handled in its own thread and returned to a common `Receiver` 12 | pub struct Events { 13 | rx: tokio::sync::mpsc::Receiver, 14 | // Need to be kept around to prevent disposing the sender side. 15 | _tx: tokio::sync::mpsc::Sender, 16 | // To stop the loop 17 | stop_capture: Arc, 18 | } 19 | 20 | impl Events { 21 | /// Constructs an new instance of `Events` with the default config. 22 | pub fn new(tick_rate: Duration) -> Events { 23 | let (tx, rx) = tokio::sync::mpsc::channel(100); 24 | let stop_capture = Arc::new(AtomicBool::new(false)); 25 | 26 | let event_tx = tx.clone(); 27 | let event_stop_capture = stop_capture.clone(); 28 | tokio::spawn(async move { 29 | loop { 30 | // poll for tick rate duration, if no event, sent tick event. 31 | if crossterm::event::poll(tick_rate).unwrap() { 32 | if let crossterm::event::Event::Key(key) = crossterm::event::read().unwrap() { 33 | let key = Key::from(key); 34 | if let Err(err) = event_tx.send(InputEvent::Input(key)).await { 35 | error!("Oops!, {}", err); 36 | } 37 | } 38 | } 39 | if let Err(err) = event_tx.send(InputEvent::Tick).await { 40 | error!("Oops!, {}", err); 41 | } 42 | if event_stop_capture.load(Ordering::Relaxed) { 43 | break; 44 | } 45 | } 46 | }); 47 | 48 | Events { 49 | rx, 50 | _tx: tx, 51 | stop_capture, 52 | } 53 | } 54 | 55 | /// Attempts to read an event. 56 | pub async fn next(&mut self) -> InputEvent { 57 | self.rx.recv().await.unwrap_or(InputEvent::Tick) 58 | } 59 | 60 | /// Close 61 | pub fn close(&mut self) { 62 | self.stop_capture.store(true, Ordering::Relaxed) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/inputs/key.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display, Formatter}; 2 | 3 | use crossterm::event; 4 | 5 | /// Represents an key. 6 | #[derive(PartialEq, Eq, Clone, Copy, Hash, Debug)] 7 | pub enum Key { 8 | Space, 9 | /// Both Enter (or Return) and numpad Enter 10 | Enter, 11 | /// Tabulation key 12 | Tab, 13 | /// Backspace key 14 | Backspace, 15 | /// Escape key 16 | Esc, 17 | 18 | /// Left arrow 19 | Left, 20 | /// Right arrow 21 | Right, 22 | /// Up arrow 23 | Up, 24 | /// Down arrow 25 | Down, 26 | 27 | /// Insert key 28 | Ins, 29 | /// Delete key 30 | Delete, 31 | /// Home key 32 | Home, 33 | /// End key 34 | End, 35 | /// Page Up key 36 | PageUp, 37 | /// Page Down key 38 | PageDown, 39 | 40 | /// F0 key 41 | F0, 42 | /// F1 key 43 | F1, 44 | /// F2 key 45 | F2, 46 | /// F3 key 47 | F3, 48 | /// F4 key 49 | F4, 50 | /// F5 key 51 | F5, 52 | /// F6 key 53 | F6, 54 | /// F7 key 55 | F7, 56 | /// F8 key 57 | F8, 58 | /// F9 key 59 | F9, 60 | /// F10 key 61 | F10, 62 | /// F11 key 63 | F11, 64 | /// F12 key 65 | F12, 66 | Char(char), 67 | Ctrl(char), 68 | Alt(char), 69 | Unknown, 70 | } 71 | 72 | impl Key { 73 | /// If exit 74 | pub fn is_exit(&self) -> bool { 75 | matches!(self, Key::Ctrl('c') | Key::Char('q') | Key::Esc) 76 | } 77 | 78 | /// Returns the function key corresponding to the given number 79 | /// 80 | /// 1 -> F1, etc... 81 | /// 82 | /// # Panics 83 | /// 84 | /// If `n == 0 || n > 12` 85 | pub fn from_f(n: u8) -> Key { 86 | match n { 87 | 0 => Key::F0, 88 | 1 => Key::F1, 89 | 2 => Key::F2, 90 | 3 => Key::F3, 91 | 4 => Key::F4, 92 | 5 => Key::F5, 93 | 6 => Key::F6, 94 | 7 => Key::F7, 95 | 8 => Key::F8, 96 | 9 => Key::F9, 97 | 10 => Key::F10, 98 | 11 => Key::F11, 99 | 12 => Key::F12, 100 | _ => panic!("unknown function key: F{}", n), 101 | } 102 | } 103 | } 104 | 105 | impl Display for Key { 106 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 107 | match *self { 108 | Key::Alt(' ') => write!(f, ""), 109 | Key::Ctrl(' ') => write!(f, ""), 110 | Key::Char(' ') => write!(f, ""), 111 | Key::Space => write!(f, ""), 112 | Key::Alt(c) => write!(f, "", c), 113 | Key::Ctrl(c) => write!(f, "", c), 114 | Key::Char(c) => write!(f, "<{}>", c), 115 | _ => write!(f, "<{:?}>", self), 116 | } 117 | } 118 | } 119 | 120 | impl From for Key { 121 | fn from(key_event: event::KeyEvent) -> Self { 122 | match key_event { 123 | event::KeyEvent { 124 | code: event::KeyCode::Esc, 125 | .. 126 | } => Key::Esc, 127 | event::KeyEvent { 128 | code: event::KeyCode::Char(' '), 129 | .. 130 | } => Key::Space, 131 | event::KeyEvent { 132 | code: event::KeyCode::Backspace, 133 | .. 134 | } => Key::Backspace, 135 | event::KeyEvent { 136 | code: event::KeyCode::Left, 137 | .. 138 | } => Key::Left, 139 | event::KeyEvent { 140 | code: event::KeyCode::Right, 141 | .. 142 | } => Key::Right, 143 | event::KeyEvent { 144 | code: event::KeyCode::Up, 145 | .. 146 | } => Key::Up, 147 | event::KeyEvent { 148 | code: event::KeyCode::Down, 149 | .. 150 | } => Key::Down, 151 | event::KeyEvent { 152 | code: event::KeyCode::Home, 153 | .. 154 | } => Key::Home, 155 | event::KeyEvent { 156 | code: event::KeyCode::End, 157 | .. 158 | } => Key::End, 159 | event::KeyEvent { 160 | code: event::KeyCode::PageUp, 161 | .. 162 | } => Key::PageUp, 163 | event::KeyEvent { 164 | code: event::KeyCode::PageDown, 165 | .. 166 | } => Key::PageDown, 167 | event::KeyEvent { 168 | code: event::KeyCode::Delete, 169 | .. 170 | } => Key::Delete, 171 | event::KeyEvent { 172 | code: event::KeyCode::Insert, 173 | .. 174 | } => Key::Ins, 175 | event::KeyEvent { 176 | code: event::KeyCode::F(n), 177 | .. 178 | } => Key::from_f(n), 179 | event::KeyEvent { 180 | code: event::KeyCode::Enter, 181 | .. 182 | } => Key::Enter, 183 | event::KeyEvent { 184 | code: event::KeyCode::Tab, 185 | .. 186 | } => Key::Tab, 187 | 188 | // First check for char + modifier 189 | event::KeyEvent { 190 | code: event::KeyCode::Char(c), 191 | modifiers: event::KeyModifiers::ALT, 192 | } => Key::Alt(c), 193 | event::KeyEvent { 194 | code: event::KeyCode::Char(c), 195 | modifiers: event::KeyModifiers::CONTROL, 196 | } => Key::Ctrl(c), 197 | 198 | event::KeyEvent { 199 | code: event::KeyCode::Char(c), 200 | .. 201 | } => Key::Char(c), 202 | 203 | _ => Key::Unknown, 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/inputs/mod.rs: -------------------------------------------------------------------------------- 1 | use self::key::Key; 2 | 3 | pub mod events; 4 | pub mod key; 5 | 6 | pub enum InputEvent { 7 | /// An input event occurred. 8 | Input(Key), 9 | Tick, 10 | } 11 | -------------------------------------------------------------------------------- /src/io/handler.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use eyre::Result; 4 | use log::{error, info, warn}; 5 | use copypasta::{ClipboardContext, ClipboardProvider}; 6 | 7 | use super::IoEvent; 8 | use crate::app::{App}; 9 | 10 | /// In the IO thread, we handle IO event without blocking the UI thread 11 | pub struct IoAsyncHandler { 12 | app: Arc>, 13 | } 14 | 15 | impl IoAsyncHandler { 16 | pub fn new(app: Arc>) -> Self { 17 | Self { app } 18 | } 19 | 20 | /// We could be async here 21 | pub async fn handle_io_event(&mut self, io_event: IoEvent) { 22 | let result = match io_event { 23 | IoEvent::Initialize => self.do_initialize().await, 24 | IoEvent::ToggleWriteMode(write_mode) => self.toggle_write_mode(write_mode).await, 25 | IoEvent::OpenFile => self.open_file().await, 26 | IoEvent::SaveFile => self.save_file().await, 27 | IoEvent::NextFile => self.next_file().await, 28 | IoEvent::PreviousFile => self.previous_file().await, 29 | IoEvent::CloseFile => self.close_file().await, 30 | IoEvent::ScrollDown => self.scroll_vertical(1).await, 31 | IoEvent::ScrollUp => self.scroll_vertical(-1).await, 32 | IoEvent::ScrollLeft => self.scroll_horizontal(-1).await, 33 | IoEvent::ScrollRight => self.scroll_horizontal(1).await, 34 | }; 35 | 36 | if let Err(err) = result { 37 | error!("Oops, something wrong happen: {:?}", err); 38 | } 39 | 40 | let mut app = self.app.lock().await; 41 | app.loaded(); 42 | } 43 | 44 | /// Initialize App 45 | async fn do_initialize(&mut self) -> Result<()> { 46 | info!("🚀 Initialize the application"); 47 | let mut app = self.app.lock().await; 48 | app.initialized(); // we could update the app state 49 | info!("👍 Application initialized"); 50 | Ok(()) 51 | } 52 | 53 | /// Toggle between Write and Input mode 54 | async fn toggle_write_mode(&mut self, new_write_mode: bool) -> Result<()> { 55 | info!("Setting Write Mode to {:?}...", new_write_mode); 56 | // Notify the app for having slept 57 | let mut app = self.app.lock().await; 58 | app.toggle_write_mode(new_write_mode); 59 | Ok(()) 60 | } 61 | 62 | /// Open a file 63 | async fn open_file(&mut self) -> Result<()> { 64 | let mut ctx = ClipboardContext::new().unwrap(); 65 | if let Some(clipboard_text) = ctx.get_contents().ok() { 66 | let mut app = self.app.lock().await; 67 | let result = app.open_files_data_mut().open_file(&clipboard_text); 68 | match result { 69 | Ok(()) => { 70 | info!("📄 Opened file: {}", clipboard_text); 71 | Ok(()) 72 | }, 73 | Err(err) => { 74 | error!("📄 Failed to open file: {}", err); 75 | Ok(()) 76 | } 77 | } 78 | } else { 79 | warn!("📄 Open file: "); 80 | Ok(()) 81 | } 82 | } 83 | 84 | /// Close the file 85 | async fn close_file(&mut self) -> Result<()> { 86 | let mut app = self.app.lock().await; 87 | let current_opened_file_path = app.open_files_data_mut().get_currently_selected_file_path(); 88 | let result = app.open_files_data_mut().close_file(); 89 | match result { 90 | Ok(()) => { 91 | info!("📄 Closed file: {}", current_opened_file_path); 92 | Ok(()) 93 | }, 94 | Err(err) => { 95 | error!("📄 Failed to Close file: {}", err); 96 | Ok(()) 97 | } 98 | } 99 | } 100 | 101 | /// Save the file 102 | async fn save_file(&mut self) -> Result<()> { 103 | let mut app = self.app.lock().await; 104 | let current_opened_file_path = app.open_files_data_mut().get_currently_selected_file_path(); 105 | let result = app.open_files_data_mut().save_file(); 106 | match result { 107 | Ok(()) => { 108 | info!("📄 Saved file: {}", current_opened_file_path); 109 | Ok(()) 110 | } 111 | Err(err) => { 112 | error!("📄 Failed to save file: {}", err); 113 | Ok(()) 114 | } 115 | } 116 | } 117 | 118 | /// Next file 119 | async fn next_file(&mut self) -> Result<()> { 120 | let mut app = self.app.lock().await; 121 | app.open_files_data_mut().select_next_file(); 122 | app.reset_scroll(); 123 | Ok(()) 124 | } 125 | 126 | /// Previous file 127 | async fn previous_file(&mut self) -> Result<()> { 128 | let mut app = self.app.lock().await; 129 | app.open_files_data_mut().select_previous_file(); 130 | app.reset_scroll(); 131 | Ok(()) 132 | } 133 | 134 | /// Scroll vertical 135 | /// direction: 1 for down, -1 for up 136 | async fn scroll_vertical(&mut self, direction: i32) -> Result<()> { 137 | let mut app = self.app.lock().await; 138 | match app.scroll_horizontal(direction) { 139 | Ok(()) => { 140 | info!("↨ Scrolled vertical. Current Scroll Offset: {:?}", app.state().get_scroll_offset()); 141 | Ok(()) 142 | }, 143 | Err(err) => { 144 | error!("Failed to scroll vertical: {}", err); 145 | Ok(()) 146 | } 147 | } 148 | } 149 | 150 | /// Scroll horizontal 151 | /// direction: 1 for right, -1 for left 152 | async fn scroll_horizontal(&mut self, direction: i32) -> Result<()> { 153 | let mut app = self.app.lock().await; 154 | match app.scroll_horizontal(direction) { 155 | Ok(()) => { 156 | info!("🔛 Scrolled horizontal. Current Scroll Offset: {:?}", app.state().get_scroll_offset()); 157 | Ok(()) 158 | }, 159 | Err(err) => { 160 | error!("Failed to scroll horizonta: {}", err); 161 | Ok(()) 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/io/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod handler; 2 | // For this dummy application we only need two IO event 3 | #[derive(Debug, Clone)] 4 | pub enum IoEvent { 5 | Initialize, // Launch to initialize the application 6 | ToggleWriteMode(bool), // Toggle whether Write Mode is active 7 | OpenFile, // Open a file 8 | SaveFile, // Save a file 9 | NextFile, // Go to next file 10 | PreviousFile, // Go to previous file 11 | CloseFile, // Close the current file 12 | ScrollDown, // Scroll down 13 | ScrollUp, // Scroll up 14 | ScrollLeft, // Scroll left 15 | ScrollRight, // Scroll right 16 | } 17 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::io::stdout; 2 | use std::sync::Arc; 3 | use std::time::Duration; 4 | 5 | use app::{App, AppReturn}; 6 | use eyre::Result; 7 | use inputs::events::Events; 8 | use inputs::InputEvent; 9 | use io::IoEvent; 10 | use tui::backend::CrosstermBackend; 11 | use tui::Terminal; 12 | 13 | use crate::app::ui; 14 | 15 | pub mod app; 16 | pub mod inputs; 17 | pub mod io; 18 | 19 | pub async fn start_ui(app: &Arc>) -> Result<()> { 20 | // Configure Crossterm backend for tui 21 | let stdout = stdout(); 22 | crossterm::terminal::enable_raw_mode()?; 23 | let backend = CrosstermBackend::new(stdout); 24 | let mut terminal = Terminal::new(backend)?; 25 | terminal.clear()?; 26 | terminal.hide_cursor()?; 27 | 28 | // User event handler 29 | let tick_rate = Duration::from_millis(200); 30 | let mut events = Events::new(tick_rate); 31 | 32 | // Trigger state change from Init to Initialized 33 | { 34 | let mut app = app.lock().await; 35 | // Here we assume the the first load is a long task 36 | app.dispatch(IoEvent::Initialize).await; 37 | } 38 | 39 | loop { 40 | let mut app = app.lock().await; 41 | 42 | // Render 43 | terminal.draw(|rect| ui::draw(rect, &mut app))?; 44 | 45 | // Handle inputs 46 | let result = match events.next().await { 47 | InputEvent::Input(key) => app.do_action(key).await, 48 | InputEvent::Tick => AppReturn::Continue, 49 | }; 50 | // Check if we should exit 51 | if result == AppReturn::Exit { 52 | events.close(); 53 | break; 54 | } 55 | } 56 | 57 | // Restore the terminal and close application 58 | terminal.clear()?; 59 | terminal.show_cursor()?; 60 | crossterm::terminal::disable_raw_mode()?; 61 | 62 | Ok(()) 63 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use eyre::Result; 4 | use log::LevelFilter; 5 | use rust_text_editor::app::App; 6 | use rust_text_editor::io::handler::IoAsyncHandler; 7 | use rust_text_editor::io::IoEvent; 8 | use rust_text_editor::start_ui; 9 | 10 | #[tokio::main] 11 | async fn main() -> Result<()> { 12 | let (sync_io_tx, mut sync_io_rx) = tokio::sync::mpsc::channel::(100); 13 | 14 | // We need to share the App between thread 15 | let app = Arc::new(tokio::sync::Mutex::new(App::new(sync_io_tx.clone()))); 16 | let app_ui = Arc::clone(&app); 17 | 18 | // Configure log 19 | tui_logger::init_logger(LevelFilter::Debug).unwrap(); 20 | tui_logger::set_default_level(log::LevelFilter::Debug); 21 | 22 | // Handle IO in a specifc thread 23 | tokio::spawn(async move { 24 | let mut handler = IoAsyncHandler::new(app); 25 | while let Some(io_event) = sync_io_rx.recv().await { 26 | handler.handle_io_event(io_event).await; 27 | } 28 | }); 29 | 30 | start_ui(&app_ui).await?; 31 | 32 | Ok(()) 33 | } 34 | --------------------------------------------------------------------------------