From 58f5d4505db91948886022c0c6cd613af021b242 Mon Sep 17 00:00:00 2001 From: ExilProductions Date: Wed, 21 Jan 2026 17:37:37 +0100 Subject: [PATCH] Initial Commit --- .gitignore | 1 + Cargo.lock | 1761 +++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 25 + src/app.rs | 446 +++++++++++++ src/buffer.rs | 688 +++++++++++++++++++ src/config.rs | 101 +++ src/editor.rs | 314 +++++++++ src/input.rs | 8 + src/main.rs | 69 ++ src/state.rs | 117 ++++ src/ui.rs | 825 +++++++++++++++++++++++ termcode.svg | 19 + 12 files changed, 4374 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/app.rs create mode 100644 src/buffer.rs create mode 100644 src/config.rs create mode 100644 src/editor.rs create mode 100644 src/input.rs create mode 100644 src/main.rs create mode 100644 src/state.rs create mode 100644 src/ui.rs create mode 100644 termcode.svg diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4cbcc43 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1761 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "TermCode" +version = "0.1.0" +dependencies = [ + "anyhow", + "arboard", + "chrono", + "clap", + "crossterm", + "dirs", + "path-absolutize", + "ratatui", + "ropey", + "serde", + "tempfile", + "tokio", + "toml", + "unicode-width 0.1.14", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags", + "objc2", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.3", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "path-absolutize" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" +dependencies = [ + "once_cell", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "ropey" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" +dependencies = [ + "smallvec", + "str_indices", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str_indices" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.3", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "zerocopy" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4017713 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "TermCode" +version = "0.1.0" +edition = "2021" + +[dependencies] +ratatui = "0.29" +crossterm = "0.28" +tokio = { version = "1.35", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" +ropey = "1.6" +anyhow = "1.0" +unicode-width = "0.1" +dirs = "5.0" +path-absolutize = "3.1" +clap = { version = "4.4", features = ["derive"] } +arboard = "3.3" +chrono = "0.4" + +[dev-dependencies] +tempfile = "3.8" + +[package.metadata.appimage] +icon = "termcode.svg" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..1d566ab --- /dev/null +++ b/src/app.rs @@ -0,0 +1,446 @@ +use crate::buffer::Buffer; +use crate::config::Config; +use crate::editor::Editor; +use crate::input::InputMode; +use crate::state::AppState; +use crate::ui::UI; +use anyhow::Result; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers}; +use ratatui::{backend::Backend, Terminal}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct App { + state: Arc>, + editor: Editor, + ui: UI, + _config: Config, +} + +impl App { + pub async fn new(files: Vec, config_path: Option) -> Result { + let config = if let Some(path) = config_path.clone() { + Config::load(Some(path))? + } else { + let default_path = Config::default_path(); + if !default_path.exists() { + let default_config = Config::default(); + default_config.save(None)?; // Create default config file + println!("Created default config at: {}", default_path.display()); + } + Config::load(None)? + }; + let state = Arc::new(RwLock::new(AppState::new())); + + let editor = Editor::new(state.clone(), config.clone()).await?; + + let mut app = App { + state: state.clone(), + editor, + ui: UI::new(), + _config: config, + }; + + for file_path in files { + if let Err(e) = app.open_file(file_path).await { + eprintln!("Failed to open file: {:?}", e); + } + } + + Ok(app) + } + + pub async fn open_file(&mut self, path: PathBuf) -> Result<()> { + let buffer = Buffer::from_path(&path).await?; + self.editor.add_buffer(buffer).await; + Ok(()) + } + + pub async fn run(&mut self, terminal: &mut Terminal) -> Result<()> { + use std::time::Duration; + + loop { + // Process tick for message timeouts + { + let mut state = self.state.write().await; + state.tick_message(); + } + + // Sync cursor from current buffer to state before drawing + self.sync_cursor_to_state().await; + + let state = (*self.state.read().await).clone(); + let editor_ref = &self.editor; + let ui_ref = &mut self.ui; + + terminal.draw(|f| ui_ref.draw(f, editor_ref, &state))?; + + // Poll for events with a small timeout + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if self.handle_key_event(key).await? { + break; + } + } + } + } + + Ok(()) + } + + async fn sync_cursor_to_state(&mut self) { + if let Some(buffer) = self.editor.current_buffer() { + let mut state = self.state.write().await; + state.set_cursor_position(buffer.cursor_line, buffer.cursor_col); + state.set_scroll_position(buffer.scroll_line, buffer.scroll_col); + state.set_file_modified(buffer.modified); + } + } + + async fn handle_key_event(&mut self, key: KeyEvent) -> Result { + let input_mode = { + let state = self.state.read().await; + state.input_mode + }; + + match input_mode { + InputMode::Normal => self.handle_normal_mode(key).await, + InputMode::Insert => self.handle_insert_mode(key).await, + InputMode::Command => { + let mut state = self.state.write().await; + self.ui.handle_key(key, &mut state); + Ok(false) + } + InputMode::GotoLine => { + let mut state = self.state.write().await; + if self.ui.handle_key(key, &mut state) { + drop(state); + self.sync_state_to_buffer().await; + } + Ok(false) + } + InputMode::CommandPalette => self.handle_command_palette(key).await, + } + } + + async fn sync_state_to_buffer(&mut self) { + let state = self.state.read().await; + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.cursor_line = state.cursor_line; + buffer.cursor_col = state.cursor_col; + buffer.scroll_line = state.scroll_line; + buffer.scroll_col = state.scroll_col; + } + } + + async fn handle_normal_mode(&mut self, key: KeyEvent) -> Result { + match (key.code, key.modifiers) { + (KeyCode::Char('q'), KeyModifiers::CONTROL) => return Ok(true), + (KeyCode::Char('s'), KeyModifiers::CONTROL) => { + self.editor.save_current_buffer().await?; + } + (KeyCode::Char('o'), KeyModifiers::CONTROL) => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Command); + self.ui.command_buffer = "open ".to_string(); + } + (KeyCode::Char('w'), KeyModifiers::CONTROL) => { + self.editor.close_current_buffer().await?; + } + (KeyCode::Char('p'), KeyModifiers::CONTROL) => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::CommandPalette); + self.ui.command_palette_selected = 0; + } + (KeyCode::Char('b'), KeyModifiers::CONTROL) => { + let mut state = self.state.write().await; + state.toggle_sidebar(); + } + // Insert mode + (KeyCode::Char('i'), KeyModifiers::NONE) => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Insert); + state.set_message("-- INSERT --".to_string()); + } + // Append mode + (KeyCode::Char('a'), KeyModifiers::NONE) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(0, 1); + } + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Insert); + state.set_message("-- INSERT --".to_string()); + } + // Append at end of line + (KeyCode::Char('A'), KeyModifiers::NONE) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_to_end_of_line(); + } + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Insert); + state.set_message("-- INSERT --".to_string()); + } + // Open line below + (KeyCode::Char('o'), KeyModifiers::NONE) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_to_end_of_line(); + buffer.insert_newline(); + } + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Insert); + state.set_message("-- INSERT --".to_string()); + } + // Open line above + (KeyCode::Char('O'), KeyModifiers::NONE) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_to_start_of_line(); + buffer.insert_newline(); + buffer.move_cursor(-1, 0); + } + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Insert); + state.set_message("-- INSERT --".to_string()); + } + + (KeyCode::Tab, _) => { + let mut state = self.state.write().await; + if state.sidebar_visible { + let new_focus = !state.sidebar_focus; + state.set_sidebar_focus(new_focus); + } + } + (KeyCode::Char('g'), KeyModifiers::CONTROL) => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::GotoLine); + self.ui.goto_buffer.clear(); + } + (KeyCode::Char('n'), KeyModifiers::CONTROL) => { + let mut state = self.state.write().await; + state.set_message("New file - use :e ".to_string()); + } + (KeyCode::Char(','), KeyModifiers::ALT) => { + self.editor.prev_buffer(); + let mut state = self.state.write().await; + state.set_message("Previous buffer".to_string()); + } + (KeyCode::Char('.'), KeyModifiers::ALT) => { + self.editor.next_buffer(); + let mut state = self.state.write().await; + state.set_message("Next buffer".to_string()); + } + (KeyCode::Enter, _) => { + let sidebar_focus = { + let state = self.state.read().await; + state.sidebar_focus + }; + + if sidebar_focus { + if self.ui.is_selected_go_up() { + if let Some(parent) = std::env::current_dir() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + { + if let Err(e) = std::env::set_current_dir(&parent) { + eprintln!("Failed to change directory: {:?}", e); + } + self.ui.refresh_files(); + self.ui.sidebar_selected = 0; + } + } else if let Some(file_item) = self.ui.get_selected_file().cloned() { + if file_item.is_dir { + self.ui.toggle_folder_expand(self.ui.sidebar_selected); + } else { + let _ = self.open_file(file_item.path).await; + } + } + } else { + // Enter key in editor - insert newline + self.editor.handle_key(key).await?; + } + } + _ => { + let sidebar_focus = { + let state = self.state.read().await; + state.sidebar_focus + }; + + if sidebar_focus { + match key.code { + KeyCode::Up => self.ui.sidebar_up(), + KeyCode::Down => self.ui.sidebar_down(), + _ => {} + } + } else { + self.editor.handle_key(key).await?; + } + } + } + + Ok(false) + } + + async fn handle_insert_mode(&mut self, key: KeyEvent) -> Result { + match (key.code, key.modifiers) { + (KeyCode::Esc, _) => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Normal); + state.set_message("".to_string()); + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(0, -1); + } + } + (KeyCode::Backspace, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.delete_backward(); + } + } + (KeyCode::Delete, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.delete_forward(); + } + } + (KeyCode::Enter, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.insert_newline(); + } + } + (KeyCode::Tab, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.insert_tab(); + } + } + (KeyCode::Up, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(-1, 0); + } + } + (KeyCode::Down, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(1, 0); + } + } + (KeyCode::Left, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(0, -1); + } + } + (KeyCode::Right, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_cursor(0, 1); + } + } + (KeyCode::Home, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_to_start_of_line(); + } + } + (KeyCode::End, _) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.move_to_end_of_line(); + } + } + (KeyCode::Char('z'), KeyModifiers::CONTROL) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.undo(); + } + } + (KeyCode::Char('y'), KeyModifiers::CONTROL) => { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.redo(); + } + } + (KeyCode::Char('v'), KeyModifiers::CONTROL) => { + let paste_text = self.editor.get_yank_buffer().cloned().unwrap_or_default(); + if !paste_text.is_empty() { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.insert_string(&paste_text); + } + } + } + (KeyCode::Char(ch), KeyModifiers::NONE) => { + if !ch.is_control() { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.insert_char(ch); + } + } + } + (KeyCode::Char(ch), KeyModifiers::ALT | KeyModifiers::CONTROL) => { + if !ch.is_control() { + if let Some(buffer) = self.editor.current_buffer_mut() { + buffer.insert_char(ch); + } + } + } + _ => {} + } + Ok(false) + } + + async fn handle_command_palette(&mut self, key: KeyEvent) -> Result { + match key.code { + KeyCode::Esc => { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Normal); + } + KeyCode::Up => { + if !self.ui.command_palette_items.is_empty() { + self.ui.command_palette_selected = + self.ui.command_palette_selected.saturating_sub(1); + } + } + KeyCode::Down => { + if !self.ui.command_palette_items.is_empty() { + let max_index = self.ui.command_palette_items.len().saturating_sub(1); + self.ui.command_palette_selected = + (self.ui.command_palette_selected + 1).min(max_index); + } + } + KeyCode::Enter => { + if let Some(cmd) = self.ui.get_selected_command().cloned() { + let should_quit = self.execute_command(&cmd).await?; + if should_quit { + return Ok(true); + } + } + let mut state = self.state.write().await; + state.set_input_mode(InputMode::Normal); + } + _ => {} + } + Ok(false) + } + + async fn execute_command(&mut self, command: &str) -> Result { + if command.starts_with("Save File") { + self.editor.save_current_buffer().await?; + let mut state = self.state.write().await; + state.set_message("File saved".to_string()); + } else if command.starts_with("Close File") { + self.editor.close_current_buffer().await?; + let mut state = self.state.write().await; + state.set_message("File closed".to_string()); + } else if command.starts_with("Toggle Sidebar") { + let mut state = self.state.write().await; + state.toggle_sidebar(); + state.set_message("Sidebar toggled".to_string()); + } else if command.starts_with("Go to Line") { + let mut state = self.state.write().await; + state.set_input_mode(InputMode::GotoLine); + self.ui.goto_buffer.clear(); + state.set_message("Enter line number".to_string()); + } else if command.starts_with("Next Buffer") { + self.editor.next_buffer(); + let mut state = self.state.write().await; + state.set_message("Next buffer".to_string()); + } else if command.starts_with("Previous Buffer") { + self.editor.prev_buffer(); + let mut state = self.state.write().await; + state.set_message("Previous buffer".to_string()); + } else if command.starts_with("Quit") { + return Ok(true); + } + + Ok(false) + } +} diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..d82b820 --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,688 @@ +use anyhow::{Context, Result}; +use ropey::Rope; +use std::path::PathBuf; + +#[derive(Clone, Debug)] +pub enum EditAction { + Insert { + position: usize, + text: String, + }, + Delete { + position: usize, + text: String, + }, + Replace { + position: usize, + old_text: String, + new_text: String, + }, +} + +impl EditAction { + pub fn invert(&self) -> EditAction { + match self { + EditAction::Insert { position, text } => EditAction::Delete { + position: *position, + text: text.clone(), + }, + EditAction::Delete { position, text } => EditAction::Insert { + position: *position, + text: text.clone(), + }, + EditAction::Replace { + position, + old_text, + new_text, + } => EditAction::Replace { + position: *position, + old_text: new_text.clone(), + new_text: old_text.clone(), + }, + } + } +} + +pub struct EditHistory { + undo_stack: Vec, + redo_stack: Vec, + max_history: usize, +} + +impl EditHistory { + pub fn new(max_history: usize) -> Self { + EditHistory { + undo_stack: Vec::with_capacity(max_history), + redo_stack: Vec::with_capacity(max_history), + max_history, + } + } + + pub fn push(&mut self, action: EditAction) { + self.undo_stack.push(action); + self.redo_stack.clear(); + if self.undo_stack.len() > self.max_history { + self.undo_stack.remove(0); + } + } + + pub fn undo(&mut self, rope: &mut Rope) -> Option { + if let Some(action) = self.undo_stack.pop() { + let inverted = action.invert(); + Self::apply_action(rope, &inverted); + self.redo_stack.push(action.clone()); + Some(action) + } else { + None + } + } + + pub fn redo(&mut self, rope: &mut Rope) -> Option { + if let Some(action) = self.redo_stack.pop() { + let inverted = action.invert(); + Self::apply_action(rope, &inverted); + self.undo_stack.push(action.clone()); + Some(action) + } else { + None + } + } + + fn apply_action(rope: &mut Rope, action: &EditAction) { + match action { + EditAction::Insert { position, text } => { + rope.insert(*position, text); + } + EditAction::Delete { position, text } => { + let char_count = text.chars().count(); + if char_count > 0 { + rope.remove(*position..*position + char_count); + } + } + EditAction::Replace { + position, + old_text, + new_text, + } => { + let old_len = old_text.chars().count(); + if old_len > 0 { + rope.remove(*position..*position + old_len); + } + if !new_text.is_empty() { + rope.insert(*position, new_text); + } + } + } + } + + pub fn can_undo(&self) -> bool { + !self.undo_stack.is_empty() + } + + pub fn can_redo(&self) -> bool { + !self.redo_stack.is_empty() + } + + pub fn clear(&mut self) { + self.undo_stack.clear(); + self.redo_stack.clear(); + } +} + +pub struct Buffer { + pub rope: Rope, + pub path: Option, + pub modified: bool, + pub cursor_line: usize, + pub cursor_col: usize, + pub scroll_line: usize, + pub scroll_col: usize, + pub history: EditHistory, + pub last_save_revision: usize, +} + +impl Buffer { + pub fn new() -> Self { + Buffer { + rope: Rope::new(), + path: None, + modified: false, + cursor_line: 0, + cursor_col: 0, + scroll_line: 0, + scroll_col: 0, + history: EditHistory::new(500), + last_save_revision: 0, + } + } + + pub async fn from_path(path: &PathBuf) -> Result { + let content = tokio::fs::read_to_string(path) + .await + .with_context(|| format!("Failed to read file: {}", path.display()))?; + + let rope = Rope::from(&content as &str); + + Ok(Buffer { + rope, + path: Some(path.clone()), + modified: false, + cursor_line: 0, + cursor_col: 0, + scroll_line: 0, + scroll_col: 0, + history: EditHistory::new(500), + last_save_revision: 0, + }) + } + + pub fn save(&mut self) -> Result<()> { + if let Some(ref path) = self.path { + let content = self.rope.to_string(); + std::fs::write(path, content) + .with_context(|| format!("Failed to write file: {}", path.display()))?; + self.modified = false; + self.last_save_revision = self.rope.len_chars(); + Ok(()) + } else { + Err(anyhow::anyhow!("No file path set for buffer")) + } + } + + pub fn is_modified(&self) -> bool { + self.modified + } + + pub fn line_count(&self) -> usize { + self.rope.len_lines() + } + + pub fn get_line(&self, line: usize) -> Option { + if line < self.line_count() { + Some(self.rope.line(line).to_string()) + } else { + None + } + } + + pub fn line_to_char(&self, line: usize) -> usize { + self.rope.line_to_char(line) + } + + pub fn char_to_line(&self, char_idx: usize) -> usize { + self.rope.char_to_line(char_idx) + } + + pub fn move_cursor(&mut self, line_delta: i32, col_delta: i32) { + let new_line = (self.cursor_line as i32 + line_delta).max(0) as usize; + let line_count = self.line_count(); + self.cursor_line = new_line.min(line_count.saturating_sub(1)); + + let line_content = self.rope.line(self.cursor_line).to_string(); + let line_len = line_content.trim_end_matches('\n').len(); + + if line_delta != 0 { + self.cursor_col = self.cursor_col.min(line_len); + } else { + let new_col = (self.cursor_col as i32 + col_delta).max(0) as usize; + self.cursor_col = new_col.min(line_len); + } + } + + pub fn move_to_start_of_line(&mut self) { + self.cursor_col = 0; + } + + pub fn move_to_end_of_line(&mut self) { + let line_content = self.rope.line(self.cursor_line).to_string(); + self.cursor_col = line_content.trim_end_matches('\n').len(); + } + + pub fn scroll(&mut self, delta: i32) { + let new_scroll = (self.scroll_line as i32 + delta).max(0) as usize; + self.scroll_line = new_scroll; + } + + pub fn move_cursor_to(&mut self, line: usize, col: usize) { + let line_count = self.line_count(); + self.cursor_line = line.min(line_count.saturating_sub(1)); + let line_content = self.rope.line(self.cursor_line).to_string(); + let line_len = line_content.trim_end_matches('\n').len(); + self.cursor_col = col.min(line_len); + } + + pub fn move_to_line_start(&mut self) { + self.cursor_col = 0; + } + + pub fn move_to_line_end(&mut self) { + let line_content = self.rope.line(self.cursor_line).to_string(); + self.cursor_col = line_content.trim_end_matches('\n').len(); + } + + pub fn move_word_forward(&mut self) { + let char_idx = self.line_to_char(self.cursor_line) + self.cursor_col; + let text_after: String = self + .rope + .get_slice(char_idx..) + .map(|s| s.to_string()) + .unwrap_or_default(); + let text_len = text_after.len(); + + let mut new_pos = char_idx; + let mut found_non_word = false; + + for (idx, ch) in text_after.char_indices() { + if ch.is_alphanumeric() || ch == '_' { + if found_non_word { + new_pos = char_idx + idx; + break; + } + } else { + found_non_word = true; + } + } + + if new_pos < char_idx + text_len { + self.cursor_line = self.char_to_line(new_pos); + self.cursor_col = new_pos - self.line_to_char(self.cursor_line); + } + } + + pub fn move_word_backward(&mut self) { + let char_idx = self.line_to_char(self.cursor_line) + self.cursor_col; + if char_idx == 0 { + return; + } + + let text_before: String = self + .rope + .get_slice(0..char_idx) + .map(|s| s.to_string()) + .unwrap_or_default(); + let mut new_pos = 0; + let mut in_word = false; + + for (idx, ch) in text_before.char_indices() { + if ch.is_alphanumeric() || ch == '_' { + in_word = true; + } else if in_word { + new_pos = idx; + in_word = false; + } + } + + self.cursor_line = self.char_to_line(new_pos); + self.cursor_col = new_pos - self.line_to_char(self.cursor_line); + } + + pub fn cursor_position(&self) -> usize { + self.line_to_char(self.cursor_line) + self.cursor_col + } + + pub fn insert_char(&mut self, ch: char) { + let pos = self.cursor_position(); + self.rope.insert(pos, &ch.to_string()); + self.move_cursor(0, 1); + self.modified = true; + } + + pub fn insert_string(&mut self, text: &str) { + if text.is_empty() { + return; + } + let pos = self.cursor_position(); + self.rope.insert(pos, text); + let char_count = text.chars().count(); + self.move_cursor(0, char_count as i32); + self.modified = true; + } + + pub fn insert_newline(&mut self) { + let pos = self.cursor_position(); + self.rope.insert(pos, "\n"); + self.move_cursor(1, 0); + self.move_to_line_start(); + self.modified = true; + } + + pub fn insert_tab(&mut self) { + self.insert_string(" "); + } + + pub fn delete_backward(&mut self) { + if self.cursor_col > 0 { + let pos = self.cursor_position(); + let char_idx = pos - 1; + self.rope.remove(char_idx..pos); + self.move_cursor(0, -1); + self.modified = true; + } else if self.cursor_line > 0 { + let current_line_start = self.line_to_char(self.cursor_line); + let prev_line_end = self.line_to_char(self.cursor_line - 1); + + if current_line_start > prev_line_end { + self.rope.remove(prev_line_end..current_line_start); + self.move_cursor(-1, 0); + let new_line_count = self.line_count(); + if self.cursor_line < new_line_count { + let prev_line = self.rope.line(self.cursor_line).to_string(); + self.cursor_col = prev_line.trim_end_matches('\n').len(); + } else { + self.cursor_col = 0; + } + self.modified = true; + } + } + } + + pub fn delete_forward(&mut self) { + let pos = self.cursor_position(); + let line_count = self.line_count(); + let line_len = self.rope.line(self.cursor_line).len_chars(); + + if self.cursor_col == 0 && self.cursor_line > 0 { + let current_line_start = self.line_to_char(self.cursor_line); + if current_line_start > 0 { + self.rope.remove(current_line_start - 1..current_line_start); + self.cursor_line -= 1; + let prev_line = self.rope.line(self.cursor_line).to_string(); + self.cursor_col = prev_line.trim_end_matches('\n').len(); + self.modified = true; + } + } else if self.cursor_col == line_len - 1 && self.cursor_line < line_count - 1 { + self.rope.remove(pos..pos + 1); + self.modified = true; + } else if self.cursor_col < line_len - 1 { + self.rope.remove(pos..pos + 1); + self.modified = true; + } + } + + pub fn delete_word_backward(&mut self) -> String { + let start_pos = self.cursor_position(); + if start_pos == 0 { + return String::new(); + } + + let mut new_col = self.cursor_col; + let mut found_word = false; + let line = self.rope.line(self.cursor_line).to_string(); + let line_start = self.line_to_char(self.cursor_line); + + for i in (0..self.cursor_col).rev() { + let ch = line.chars().nth(i).unwrap(); + if ch.is_alphanumeric() || ch == '_' { + new_col = i; + found_word = true; + } else if found_word { + break; + } + } + + let delete_start = line_start + new_col; + let deleted: String = self + .rope + .get_slice(delete_start..start_pos) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.rope.remove(delete_start..start_pos); + self.cursor_col = new_col; + self.modified = true; + deleted + } + + pub fn delete_word_forward(&mut self) -> String { + let start_pos = self.cursor_position(); + let line_count = self.line_count(); + let line_start = self.line_to_char(self.cursor_line); + let line_len = self.rope.line(self.cursor_line).len_chars(); + + let mut delete_end = start_pos; + let mut found_word = false; + + if self.cursor_col < line_len.saturating_sub(1) { + for i in self.cursor_col..line_len { + let ch = self + .rope + .line(self.cursor_line) + .chars() + .nth(i) + .map(|c| { + if c.is_alphanumeric() || c == '_' { + true + } else { + false + } + }) + .unwrap_or(false); + if ch { + found_word = true; + } else if found_word { + delete_end = line_start + i; + break; + } + } + if found_word { + delete_end = line_start + line_len; + } + } + + if delete_end > start_pos { + let deleted: String = self + .rope + .get_slice(start_pos..delete_end) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.rope.remove(start_pos..delete_end); + self.modified = true; + deleted + } else { + String::new() + } + } + + pub fn delete_current_line(&mut self) -> String { + let line_start = self.line_to_char(self.cursor_line); + let line_count = self.line_count(); + let line_len = self.rope.line(self.cursor_line).len_chars(); + + let line_end = if self.cursor_line < line_count - 1 { + line_start + line_len + } else { + line_start + line_len.saturating_sub(1) + }; + + let deleted: String = self + .rope + .get_slice(line_start..line_end) + .map(|s| s.to_string()) + .unwrap_or_default(); + self.rope.remove(line_start..line_end); + + if self.cursor_line >= self.line_count() && self.cursor_line > 0 { + self.cursor_line = self.cursor_line.saturating_sub(1); + } + self.move_to_line_end(); + self.modified = true; + deleted + } + + pub fn yank(&mut self) -> String { + let pos = self.cursor_position(); + self.rope + .get_slice(pos..pos) + .map(|s| s.to_string()) + .unwrap_or_default() + } + + pub fn yank_line(&mut self) -> String { + let line_start = self.line_to_char(self.cursor_line); + let line_count = self.line_count(); + let line_len = self.rope.line(self.cursor_line).len_chars(); + let line_end = if self.cursor_line < line_count - 1 { + line_start + line_len + } else { + line_start + line_len.saturating_sub(1) + }; + self.rope + .get_slice(line_start..line_end) + .map(|s| s.to_string()) + .unwrap_or_default() + } + + pub fn replace_selection(&mut self, text: &str) { + let pos = self.cursor_position(); + let end_pos = pos + text.len(); + self.rope.remove(pos..end_pos); + self.rope.insert(pos, text); + self.move_cursor(0, text.chars().count() as i32); + self.modified = true; + } + + pub fn join_with_next_line(&mut self) { + let line_count = self.line_count(); + if self.cursor_line >= line_count - 1 { + return; + } + + let current_line_start = self.line_to_char(self.cursor_line); + let next_line_start = self.line_to_char(self.cursor_line + 1); + + self.rope.remove(next_line_start - 1..next_line_start); + self.move_to_line_end(); + self.modified = true; + } + + pub fn swap_chars(&mut self) { + let pos = self.cursor_position(); + if pos == 0 { + return; + } + + let chars: Vec = self.rope.chars_at(pos - 1).take(2).collect(); + if chars.len() < 2 { + return; + } + + let char1 = chars[0]; + let char2 = chars[1]; + + self.rope.remove(pos - 1..pos + 1); + self.rope.insert(pos - 1, &format!("{}{}", char2, char1)); + } + + pub fn undo(&mut self) -> bool { + if self.history.undo(&mut self.rope).is_some() { + let new_pos = self.cursor_position(); + self.cursor_line = self.char_to_line(new_pos); + self.cursor_col = new_pos - self.line_to_char(self.cursor_line); + self.modified = self.history.undo_stack.len() > 0 + || self.rope.len_chars() != self.last_save_revision; + true + } else { + false + } + } + + pub fn redo(&mut self) -> bool { + if self.history.redo(&mut self.rope).is_some() { + let new_pos = self.cursor_position(); + self.cursor_line = self.char_to_line(new_pos); + self.cursor_col = new_pos - self.line_to_char(self.cursor_line); + self.modified = self.history.undo_stack.len() > 0 + || self.rope.len_chars() != self.last_save_revision; + true + } else { + false + } + } + + pub fn can_undo(&self) -> bool { + self.history.can_undo() + } + + pub fn can_redo(&self) -> bool { + self.history.can_redo() + } + + pub fn clear_history(&mut self) { + self.history.clear(); + self.modified = self.rope.len_chars() != self.last_save_revision; + } + + pub fn find_matching_bracket(&self) -> Option<(usize, usize)> { + let brackets = [('(', ')'), ('[', ']'), ('{', '}')]; + let chars: Vec = self.rope.to_string().chars().collect(); + + let (open_char, close_char) = if let Some(b) = brackets + .iter() + .find(|b| self.cursor_col < chars.len() && chars[self.cursor_col] == b.0) + { + (b.0, b.1) + } else if let Some(b) = brackets + .iter() + .find(|b| self.cursor_col > 0 && chars[self.cursor_col - 1] == b.1) + { + (b.0, b.1) + } else { + return None; + }; + + let is_open = chars.get(self.cursor_col) == Some(&open_char); + let start_line = self.line_to_char(self.cursor_line); + let start_col = if is_open { + self.cursor_col + } else { + if self.cursor_col > 0 { + self.cursor_col - 1 + } else { + return None; + } + }; + + let mut pos = start_line + start_col; + let mut depth = 1; + let forward = is_open; + + while pos < chars.len() { + let ch = chars[pos]; + if forward { + if ch == open_char { + depth += 1; + } else if ch == close_char { + depth -= 1; + if depth == 0 { + let line = self.rope.char_to_line(pos); + let col = pos - self.rope.line_to_char(line); + return Some((line, col)); + } + } + pos += 1; + } else { + if ch == close_char { + depth += 1; + } else if ch == open_char { + depth -= 1; + if depth == 0 { + let line = self.rope.char_to_line(pos); + let col = pos - self.rope.line_to_char(line); + return Some((line, col)); + } + } + if pos == 0 { + break; + } + pos -= 1; + } + } + + None + } +} + +impl Default for Buffer { + fn default() -> Self { + Buffer::new() + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..926912f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,101 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub editor: EditorConfig, + pub ui: UIConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EditorConfig { + pub word_wrap: bool, + pub line_numbers: bool, + pub relative_line_numbers: bool, + pub auto_save: bool, + pub auto_save_delay: u64, + pub scroll_offset: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UIConfig { + pub sidebar_width: u16, + pub status_bar_height: u16, + pub command_palette_height: u16, + pub tab_bar_height: u16, + pub mouse_enabled: bool, + pub show_whitespace: bool, +} + +impl Default for Config { + fn default() -> Self { + Config { + editor: EditorConfig::default(), + ui: UIConfig::default(), + } + } +} + +impl Default for EditorConfig { + fn default() -> Self { + EditorConfig { + word_wrap: false, + line_numbers: true, + relative_line_numbers: false, + auto_save: false, + auto_save_delay: 5000, + scroll_offset: 5, + } + } +} + +impl Default for UIConfig { + fn default() -> Self { + UIConfig { + sidebar_width: 30, + status_bar_height: 1, + command_palette_height: 10, + tab_bar_height: 1, + mouse_enabled: true, + show_whitespace: false, + } + } +} + +impl Config { + pub fn load(path: Option) -> Result { + let config_path = path.unwrap_or_else(Self::default_path); + + if !config_path.exists() { + return Ok(Config::default()); + } + + let content = fs::read_to_string(&config_path).context("Failed to read config file")?; + + let config: Config = toml::from_str(&content).context("Failed to parse config file")?; + + Ok(config) + } + + pub fn default_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("termcode") + .join("config.toml") + } + + pub fn save(&self, path: Option) -> Result<()> { + let config_path = path.unwrap_or_else(Self::default_path); + + if let Some(parent) = config_path.parent() { + fs::create_dir_all(parent)?; + } + + let content = toml::to_string_pretty(self)?; + fs::write(&config_path, content)?; + + Ok(()) + } +} diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..751adb1 --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,314 @@ +use crate::buffer::Buffer; +use crate::config::{Config, EditorConfig}; +use crate::input::InputMode; +use crate::state::AppState; +use anyhow::Result; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct Editor { + buffers: Vec, + current_buffer: Option, + state: Arc>, + config: EditorConfig, + yank_buffer: String, +} + +impl Editor { + pub async fn new(state: Arc>, config: Config) -> Result { + Ok(Editor { + buffers: Vec::new(), + current_buffer: None, + state, + config: config.editor, + yank_buffer: String::new(), + }) + } + + pub async fn add_buffer(&mut self, buffer: Buffer) { + self.current_buffer = Some(self.buffers.len()); + self.buffers.push(buffer); + + let file_name = self + .buffers + .last() + .and_then(|b| b.path.clone()) + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| "Untitled".to_string()); + + let mut state = self.state.write().await; + state.set_current_file_name(file_name); + } + + pub fn current_buffer_mut(&mut self) -> Option<&mut Buffer> { + self.current_buffer + .and_then(|idx| self.buffers.get_mut(idx)) + } + + pub fn current_buffer(&self) -> Option<&Buffer> { + self.current_buffer.and_then(|idx| self.buffers.get(idx)) + } + + pub async fn save_current_buffer(&mut self) -> Result<()> { + if let Some(buffer) = self.current_buffer_mut() { + buffer.save()?; + buffer.modified = false; + } + let mut state = self.state.write().await; + state.set_message("File saved".to_string()); + Ok(()) + } + + pub fn get_yank_buffer(&self) -> Option<&String> { + if self.yank_buffer.is_empty() { + None + } else { + Some(&self.yank_buffer) + } + } + + pub fn set_yank_buffer(&mut self, text: String) { + self.yank_buffer = text; + } + + pub async fn handle_key(&mut self, key: KeyEvent) -> Result<()> { + let input_mode = { + let state = self.state.read().await; + state.input_mode + }; + + if input_mode != InputMode::Normal { + return Ok(()); + } + + let page_size = self.config.scroll_offset * 2; + let mut message: Option = None; + let mut input_mode_change: Option = None; + let mut yank_text: Option = None; + let yank_buffer_clone = self.yank_buffer.clone(); + + if let Some(buffer) = self.current_buffer_mut() { + match (key.code, key.modifiers) { + (KeyCode::Left, KeyModifiers::NONE) => buffer.move_cursor(0, -1), + (KeyCode::Right, KeyModifiers::NONE) => buffer.move_cursor(0, 1), + (KeyCode::Up, KeyModifiers::NONE) => buffer.move_cursor(-1, 0), + (KeyCode::Down, KeyModifiers::NONE) => buffer.move_cursor(1, 0), + (KeyCode::Home, KeyModifiers::NONE) => buffer.move_to_start_of_line(), + (KeyCode::End, KeyModifiers::NONE) => buffer.move_to_end_of_line(), + (KeyCode::PageUp, KeyModifiers::NONE) => { + buffer.move_cursor(-(page_size as i32), 0); + buffer.scroll(-(page_size as i32)); + } + (KeyCode::PageDown, KeyModifiers::NONE) => { + buffer.move_cursor(page_size as i32, 0); + buffer.scroll(page_size as i32); + } + (KeyCode::Left, KeyModifiers::ALT) => buffer.move_word_backward(), + (KeyCode::Right, KeyModifiers::ALT) => buffer.move_word_forward(), + (KeyCode::Enter, KeyModifiers::NONE) => buffer.insert_newline(), + (KeyCode::Tab, KeyModifiers::NONE) => buffer.insert_tab(), + (KeyCode::Backspace, KeyModifiers::NONE) => buffer.delete_backward(), + (KeyCode::Delete, KeyModifiers::NONE) => buffer.delete_forward(), + (KeyCode::Backspace, KeyModifiers::CONTROL) => { + buffer.delete_word_backward(); + } + (KeyCode::Char('d'), KeyModifiers::ALT) => { + buffer.delete_word_forward(); + } + (KeyCode::Char('k'), KeyModifiers::CONTROL) => { + let line_start = buffer.line_to_char(buffer.cursor_line); + let current_pos = buffer.cursor_position(); + let line_len = buffer.rope.line(buffer.cursor_line).len_chars(); + let line_end = line_start + line_len.saturating_sub(1); + if current_pos < line_end { + buffer.rope.remove(current_pos..line_end); + buffer.modified = true; + } + } + (KeyCode::Char('u'), KeyModifiers::CONTROL) => { + let line_start = buffer.line_to_char(buffer.cursor_line); + let current_pos = buffer.cursor_position(); + if current_pos > line_start { + buffer.rope.remove(line_start..current_pos); + buffer.cursor_col = 0; + buffer.modified = true; + } + } + (KeyCode::Char('z'), KeyModifiers::CONTROL) => { + if buffer.undo() { + message = Some("Undo".to_string()); + } + } + (KeyCode::Char('y'), KeyModifiers::CONTROL) => { + if buffer.redo() { + message = Some("Redo".to_string()); + } + } + (KeyCode::Char('c'), KeyModifiers::CONTROL) => { + let yank = buffer.yank_line(); + if !yank.is_empty() { + yank_text = Some(yank.clone()); + if Self::copy_to_clipboard(&yank).await.is_err() { + message = Some("Copy failed".to_string()); + } else { + message = Some("Copied".to_string()); + } + } + } + (KeyCode::Char('x'), KeyModifiers::CONTROL) => { + let yank = buffer.delete_current_line(); + if !yank.is_empty() { + yank_text = Some(yank.clone()); + if Self::copy_to_clipboard(&yank).await.is_err() { + message = Some("Cut failed".to_string()); + } else { + message = Some("Cut".to_string()); + } + } + } + (KeyCode::Char('v'), KeyModifiers::CONTROL) => { + let paste_text = if !yank_buffer_clone.is_empty() { + yank_buffer_clone + } else { + match Self::paste_from_clipboard().await { + Ok(text) if !text.is_empty() => text, + _ => { + message = Some("Paste failed".to_string()); + String::new() + } + } + }; + if !paste_text.is_empty() { + buffer.insert_string(&paste_text); + message = Some("Pasted".to_string()); + } + } + (KeyCode::Char('d'), KeyModifiers::CONTROL) => { + let deleted = buffer.delete_current_line(); + if !deleted.is_empty() { + yank_text = Some(deleted); + message = Some("Line deleted".to_string()); + } + } + (KeyCode::Char('y'), KeyModifiers::ALT) => { + yank_text = Some(buffer.yank_line()); + message = Some("Line yanked".to_string()); + } + (KeyCode::Char('t'), KeyModifiers::CONTROL) => { + buffer.swap_chars(); + message = Some("Characters swapped".to_string()); + } + (KeyCode::Char('j'), KeyModifiers::CONTROL) => { + buffer.join_with_next_line(); + } + (KeyCode::Char('a'), KeyModifiers::CONTROL) => buffer.move_to_start_of_line(), + (KeyCode::Char('e'), KeyModifiers::CONTROL) => buffer.move_to_end_of_line(), + (KeyCode::Esc, KeyModifiers::NONE) => { + input_mode_change = Some(InputMode::Normal); + } + (KeyCode::Char(ch), KeyModifiers::NONE) => { + if !ch.is_control() { + buffer.insert_char(ch); + } + } + (KeyCode::Char(ch), KeyModifiers::ALT | KeyModifiers::CONTROL) => { + if !ch.is_control() { + buffer.insert_char(ch); + } + } + _ => {} + } + + let visible_height = page_size; + if buffer.cursor_line < buffer.scroll_line { + buffer.scroll_line = buffer.cursor_line.saturating_sub(4); + } + if buffer.cursor_line >= buffer.scroll_line + visible_height { + buffer.scroll_line = buffer.cursor_line.saturating_sub(visible_height - 4); + } + } + + if let Some(yank) = yank_text { + self.yank_buffer = yank; + } + + if let Some(msg) = message { + let mut state = self.state.write().await; + state.set_message(msg); + } + if let Some(mode) = input_mode_change { + let mut state = self.state.write().await; + state.set_input_mode(mode); + } + + Ok(()) + } + + async fn copy_to_clipboard(text: &str) -> Result<()> { + let text = text.to_string(); + tokio::task::spawn_blocking(move || { + let mut ctx = arboard::Clipboard::new()?; + ctx.set_text(text)?; + Ok::<(), anyhow::Error>(()) + }) + .await + .map_err(|e| anyhow::anyhow!("Clipboard task failed: {:?}", e))? + } + + async fn paste_from_clipboard() -> Result { + tokio::task::spawn_blocking(|| { + let mut ctx = arboard::Clipboard::new()?; + ctx.get_text() + .map_err(|e| anyhow::anyhow!("Failed to get clipboard: {:?}", e)) + }) + .await + .map_err(|e| anyhow::anyhow!("Clipboard task failed: {:?}", e))? + } + + pub async fn close_current_buffer(&mut self) -> Result<()> { + if let Some(idx) = self.current_buffer { + self.buffers.remove(idx); + if self.buffers.is_empty() { + self.current_buffer = None; + } else if idx >= self.buffers.len() { + self.current_buffer = Some(self.buffers.len() - 1); + } else { + self.current_buffer = Some(idx); + } + } + + let file_name = self + .current_buffer() + .and_then(|b| b.path.clone()) + .and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string())) + .unwrap_or_else(|| "Untitled".to_string()); + + let mut state = self.state.write().await; + state.set_current_file_name(file_name); + + Ok(()) + } + + pub fn next_buffer(&mut self) { + if let Some(current) = self.current_buffer { + let next = (current + 1) % self.buffers.len(); + self.current_buffer = Some(next); + } else if !self.buffers.is_empty() { + self.current_buffer = Some(0); + } + } + + pub fn prev_buffer(&mut self) { + if let Some(current) = self.current_buffer { + let prev = if current == 0 { + self.buffers.len().saturating_sub(1) + } else { + current - 1 + }; + self.current_buffer = Some(prev); + } else if !self.buffers.is_empty() { + self.current_buffer = Some(0); + } + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..05bce89 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InputMode { + Normal, + Insert, + CommandPalette, + GotoLine, + Command, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3e2b454 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,69 @@ +mod app; +mod buffer; +mod config; +mod editor; +mod input; +mod state; +mod ui; + +use anyhow::Result; +use clap::Parser; +use crossterm::{ + event::{DisableMouseCapture, EnableMouseCapture}, + execute, + terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, +}; +use ratatui::{backend::CrosstermBackend, Terminal}; +use std::io; +use std::panic; +use std::path::PathBuf; + +use app::App; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + files: Vec, + #[arg(short, long)] + config: Option, +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + + setup_panic_hook(); + + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut app = App::new(args.files, args.config).await?; + + let res = app.run(&mut terminal).await; + + disable_raw_mode()?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + )?; + terminal.show_cursor()?; + + if let Err(err) = res { + eprintln!("{:?}", err); + } + + Ok(()) +} + +fn setup_panic_hook() { + let original_hook = panic::take_hook(); + panic::set_hook(Box::new(move |panic_info| { + disable_raw_mode().unwrap(); + execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); + original_hook(panic_info); + })); +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..d6efa8f --- /dev/null +++ b/src/state.rs @@ -0,0 +1,117 @@ +use crate::config::EditorConfig; +use crate::input::InputMode; +use std::path::PathBuf; + +#[derive(Clone)] +pub struct AppState { + pub input_mode: InputMode, + pub current_file_name: String, + pub current_file_path: Option, + pub cursor_line: usize, + pub cursor_col: usize, + pub scroll_line: usize, + pub scroll_col: usize, + pub file_modified: bool, + pub message: Option, + pub message_timeout: usize, + pub sidebar_visible: bool, + pub sidebar_focus: bool, + pub command_history: Vec, + pub command_history_index: usize, + pub editor_config: EditorConfig, +} + +impl AppState { + pub fn new() -> Self { + AppState { + input_mode: InputMode::Normal, + current_file_name: String::from("Untitled"), + current_file_path: None, + cursor_line: 0, + cursor_col: 0, + scroll_line: 0, + scroll_col: 0, + file_modified: false, + message: None, + message_timeout: 0, + sidebar_visible: true, + sidebar_focus: false, + command_history: Vec::new(), + command_history_index: 0, + editor_config: EditorConfig::default(), + } + } + + pub fn set_input_mode(&mut self, mode: InputMode) { + self.input_mode = mode; + if mode != InputMode::Command { + self.command_history_index = self.command_history.len(); + } + } + + pub fn set_current_file_name(&mut self, name: String) { + self.current_file_name = name; + } + + pub fn set_cursor_position(&mut self, line: usize, col: usize) { + self.cursor_line = line; + self.cursor_col = col; + } + + pub fn set_scroll_position(&mut self, line: usize, col: usize) { + self.scroll_line = line; + self.scroll_col = col; + } + + pub fn set_file_modified(&mut self, modified: bool) { + self.file_modified = modified; + } + + pub fn set_message(&mut self, message: String) { + self.message = Some(message); + self.message_timeout = 10; + } + + pub fn tick_message(&mut self) { + if self.message_timeout > 0 { + self.message_timeout -= 1; + if self.message_timeout == 0 { + self.message = None; + } + } + } + + pub fn toggle_sidebar(&mut self) { + self.sidebar_visible = !self.sidebar_visible; + } + + pub fn set_sidebar_focus(&mut self, focus: bool) { + self.sidebar_focus = focus; + } + + pub fn add_command_to_history(&mut self, command: String) { + self.command_history.push(command); + self.command_history_index = self.command_history.len(); + } + + pub fn status_string(&self) -> String { + let mode_str = match self.input_mode { + InputMode::Normal => "NORMAL", + InputMode::Insert => "INSERT", + InputMode::CommandPalette => "CMD", + InputMode::GotoLine => "GOTO", + InputMode::Command => ":", + }; + + let modified = if self.file_modified { "[+]" } else { "" }; + + format!( + "{} {}{} | {}:{}", + self.current_file_name, + modified, + mode_str, + self.cursor_line + 1, + self.cursor_col + 1 + ) + } +} diff --git a/src/ui.rs b/src/ui.rs new file mode 100644 index 0000000..f2642e1 --- /dev/null +++ b/src/ui.rs @@ -0,0 +1,825 @@ +use crate::buffer::Buffer; +use crate::editor::Editor; +use crate::state::AppState; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::{ + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Line, Span}, + widgets::{Block, Borders, Clear, List, ListItem, Paragraph}, + Frame, +}; +use std::collections::HashSet; +use std::fs; +use std::path::PathBuf; + +#[derive(Clone)] +pub struct FileTreeItem { + pub path: PathBuf, + pub is_dir: bool, + pub depth: usize, +} + +pub struct UI { + pub command_buffer: String, + pub goto_buffer: String, + pub command_history: Vec, + pub command_history_index: usize, + pub command_palette_items: Vec, + pub command_palette_selected: usize, + pub sidebar_width: u16, + pub file_items: Vec<(PathBuf, bool)>, + pub sidebar_selected: usize, + pub expanded_folders: HashSet, + pub file_tree: Vec, + pub can_go_up: bool, +} + +impl UI { + pub fn new() -> Self { + UI { + command_buffer: String::new(), + goto_buffer: String::new(), + command_history: Vec::new(), + command_history_index: 0, + command_palette_items: Vec::new(), + command_palette_selected: 0, + sidebar_width: 25, + file_items: Vec::new(), + sidebar_selected: 0, + expanded_folders: HashSet::new(), + file_tree: Vec::new(), + can_go_up: false, + } + } + + pub fn refresh_files(&mut self) { + self.file_items.clear(); + self.file_tree.clear(); + + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + + // Fixed: properly check if we can go up + self.can_go_up = cwd.parent().is_some(); + + if let Ok(entries) = fs::read_dir(&cwd) { + let mut dirs: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + dirs.sort_by_key(|e| e.file_name()); + + for entry in dirs { + let path = entry.path(); + if entry.file_name().to_string_lossy().starts_with('.') { + continue; + } + self.file_items.push((path.clone(), path.is_dir())); + } + } + + self.build_file_tree(); + } + + fn build_file_tree(&mut self) { + self.file_tree.clear(); + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + + // Add ".." entry if we can go up + if self.can_go_up { + if let Some(parent) = cwd.parent() { + self.file_tree.push(FileTreeItem { + path: parent.to_path_buf(), + is_dir: true, + depth: 0, + }); + } + } + + self.add_directory_contents(&cwd, 0); + } + + fn add_directory_contents(&mut self, dir_path: &PathBuf, depth: usize) { + if let Ok(entries) = fs::read_dir(dir_path) { + let mut entries_vec: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + entries_vec.sort_by_key(|e| e.file_name()); + + for entry in entries_vec { + let path = entry.path(); + if entry.file_name().to_string_lossy().starts_with('.') { + continue; + } + + let is_dir = path.is_dir(); + self.file_tree.push(FileTreeItem { + path: path.clone(), + is_dir, + depth: depth + 1, + }); + + if is_dir && self.expanded_folders.contains(&path) { + self.add_directory_contents(&path, depth + 1); + } + } + } + } + + pub fn toggle_folder_expand(&mut self, index: usize) -> bool { + if index >= self.file_tree.len() || index == 0 { + return false; + } + + let item = &self.file_tree[index]; + if !item.is_dir { + return false; + } + + let path = item.path.clone(); + if self.expanded_folders.contains(&path) { + self.expanded_folders.remove(&path); + self.build_file_tree(); + false + } else { + self.expanded_folders.insert(path.clone()); + self.build_file_tree(); + true + } + } + + pub fn sidebar_down(&mut self) { + if !self.file_tree.is_empty() { + self.sidebar_selected = (self.sidebar_selected + 1).min(self.file_tree.len() - 1); + } + } + + pub fn sidebar_up(&mut self) { + self.sidebar_selected = self.sidebar_selected.saturating_sub(1); + } + + pub fn get_selected_file(&self) -> Option<&FileTreeItem> { + self.file_tree.get(self.sidebar_selected) + } + + pub fn draw(&mut self, f: &mut Frame, editor: &Editor, state: &AppState) { + self.refresh_files(); + + let size = f.area(); + + let sidebar_width = if state.sidebar_visible && state.sidebar_focus { + let max_name_len = self + .file_tree + .iter() + .map(|item| { + item.path + .file_name() + .map(|n| n.to_string_lossy().len()) + .unwrap_or(0) + }) + .max() + .unwrap_or(0); + let base_width = 24; + (max_name_len + base_width).max(24) as u16 + } else { + self.sidebar_width + }; + + let layout = if state.sidebar_visible { + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(sidebar_width), Constraint::Min(1)]) + .split(size) + } else { + Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Min(1)]) + .split(size) + }; + + let (main_area, status_area) = if state.sidebar_visible { + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(1)]) + .split(layout[1]); + (main_layout[0], main_layout[1]) + } else { + let main_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Length(1)]) + .split(layout[0]); + (main_layout[0], main_layout[1]) + }; + + if state.sidebar_visible { + self.draw_sidebar(f, layout[0], state, state.sidebar_focus); + } + + self.draw_editor(f, main_area, editor, state); + self.draw_status_bar(f, status_area, state); + + if state.input_mode == crate::input::InputMode::CommandPalette { + self.draw_command_palette(f, size); + } + } + + fn draw_sidebar(&mut self, f: &mut Frame, area: Rect, state: &AppState, focused: bool) { + let block = Block::default() + .borders(Borders::RIGHT) + .style(Style::default().bg(Color::Rgb(30, 30, 30))); + + f.render_widget(block, area); + + let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); + let cwd_display = cwd + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| "/".to_string()); + + let focused_bg = Color::Rgb(70, 70, 70); + let unfocused_bg = Color::Rgb(40, 40, 40); + + let available_width = area.width.saturating_sub(4); + let mut items: Vec = Vec::new(); + + for (i, item) in self.file_tree.iter().enumerate() { + // Check if this is the ".." entry + let is_parent = self.can_go_up && i == 0; + + let name = if is_parent { + "..".to_string() + } else { + item.path + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_else(|| item.path.to_string_lossy().to_string()) + }; + + // Check if this file is the current buffer and is modified + let modified_indicator = if !is_parent && state.file_modified { + if let Some(ref current_path) = state.current_file_path { + if item.path == *current_path { + "*" + } else { + "" + } + } else { + "" + } + } else { + "" + }; + + let is_selected = i == self.sidebar_selected; + + let expand_icon = if is_parent { + " " // No expand icon for ".." + } else if item.is_dir { + if self.expanded_folders.contains(&item.path) { + "[-] " + } else { + "[+] " + } + } else { + " " + }; + + let indent = if is_parent { + "" + } else { + &" ".repeat(item.depth) + }; + + let bg = if is_selected && focused { + focused_bg + } else if is_selected { + unfocused_bg + } else { + Color::Rgb(30, 30, 30) + }; + + let style = if is_selected && focused { + Style::default() + .bg(bg) + .fg(Color::White) + .add_modifier(Modifier::BOLD) + } else if item.is_dir || is_parent { + Style::default().bg(bg).fg(Color::Cyan) + } else { + Style::default().bg(bg).fg(Color::White) + }; + + let prefix = if is_selected { "> " } else { " " }; + + let full_text = format!( + "{}{}{}{}{}", + prefix, indent, expand_icon, name, modified_indicator + ); + let display_text = if focused || full_text.len() <= (available_width as usize) { + full_text + } else { + let prefix_len = prefix.len(); + let indent_len = indent.len(); + let expand_len = expand_icon.len(); + let reserved_len = prefix_len + indent_len + expand_len + 4; + let name_len = (available_width as usize).saturating_sub(reserved_len); + if name_len > 0 { + format!( + "{}{}{}{}...", + prefix, + indent, + expand_icon, + &name[..name_len.min(name.len())] + ) + } else { + format!("{}{}{}...", prefix, indent, expand_icon) + } + }; + + items.push(ListItem::new(Line::from(vec![Span::styled( + display_text, + style, + )]))); + } + + let list = List::new(items) + .block( + Block::default() + .title(format!("Files - {}", cwd_display)) + .borders(Borders::NONE), + ) + .style(Style::default().bg(Color::Rgb(30, 30, 30))); + + f.render_widget(list, area); + } + + fn draw_editor(&mut self, f: &mut Frame, area: Rect, editor: &Editor, state: &AppState) { + if let Some(buffer) = editor.current_buffer() { + self.draw_buffer(f, area, buffer, editor, state); + } else { + self.draw_welcome_screen(f, area); + } + } + + fn draw_buffer( + &mut self, + f: &mut Frame, + area: Rect, + buffer: &Buffer, + editor: &Editor, + state: &AppState, + ) { + let content_area = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(6), Constraint::Min(1)]) + .split(area); + + self.draw_line_numbers(f, content_area[0], buffer, state); + self.draw_code(f, content_area[1], buffer, editor, state); + } + + fn draw_line_numbers(&mut self, f: &mut Frame, area: Rect, buffer: &Buffer, state: &AppState) { + let visible_height = area.height as usize; + let start_line = state.scroll_line; + let end_line = (start_line + visible_height).min(buffer.line_count()); + + let line_numbers: Vec = (start_line..end_line) + .map(|line| { + let is_current_line = line == state.cursor_line; + let line_num = if state.editor_config.relative_line_numbers && !is_current_line { + (line as i32 - state.cursor_line as i32).abs() as usize + } else { + line + 1 + }; + + let style = if is_current_line { + Style::default() + .fg(Color::White) + .bg(Color::Rgb(60, 60, 60)) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::Rgb(120, 120, 120)) + }; + + Line::from(vec![Span::styled(format!("{:>5} ", line_num), style)]) + }) + .collect(); + + let paragraph = Paragraph::new(line_numbers) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().bg(Color::Rgb(30, 30, 30))); + + f.render_widget(paragraph, area); + } + + fn draw_code( + &mut self, + f: &mut Frame, + area: Rect, + buffer: &Buffer, + _editor: &Editor, + state: &AppState, + ) { + let visible_height = area.height as usize; + let start_line = state.scroll_line; + let end_line = (start_line + visible_height).min(buffer.line_count()); + let start_col = state.scroll_col; + let matching_bracket = buffer.find_matching_bracket(); + + let indent_guide_color = Color::Rgb(50, 50, 60); + let tab_width = 4; + let indent_guide_width = 2; + + let content_area = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Length(indent_guide_width), Constraint::Min(1)]) + .split(area); + + let indent_area = content_area[0]; + let code_area = content_area[1]; + + let guide_lines: Vec = (start_line..end_line) + .map(|line| { + let line_content = buffer.get_line(line).unwrap_or_default(); + let chars: Vec = line_content.chars().collect(); + + let indent_level = + chars + .iter() + .take_while(|c| **c == ' ' || **c == '\t') + .fold( + 0, + |acc, c| { + if *c == '\t' { + acc + tab_width + } else { + acc + 1 + } + }, + ) + / tab_width; + + let is_current_line = line == state.cursor_line; + let bg = if is_current_line { + Color::Rgb(60, 60, 60) + } else { + Color::Rgb(30, 30, 30) + }; + + let mut guides = String::new(); + for _ in 0..indent_level.min(8) { + guides.push('│'); + } + + let style = Style::default().fg(indent_guide_color).bg(bg); + + Line::from(vec![Span::styled(guides, style)]) + }) + .collect(); + + let guide_paragraph = Paragraph::new(guide_lines) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().bg(Color::Rgb(30, 30, 30))); + + f.render_widget(guide_paragraph, indent_area); + + let code_lines: Vec = (start_line..end_line) + .map(|line| { + let line_content = buffer.get_line(line).unwrap_or_default(); + let chars: Vec = line_content.chars().collect(); + + let is_current_line = line == state.cursor_line; + let cursor_col_in_line = if is_current_line { + Some(state.cursor_col) + } else { + None + }; + + let match_pos = matching_bracket.map(|(l, c)| (l, c)); + let match_col_in_line = if match_pos.map(|(l, _)| l == line).unwrap_or(false) { + Some(match_pos.unwrap().1) + } else { + None + }; + + let mut spans = Vec::new(); + let mut current_col = 0; + + for (col, ch) in chars.iter().enumerate() { + if col < start_col { + continue; + } + if current_col >= code_area.width as usize { + break; + } + + let is_cursor = cursor_col_in_line == Some(col); + let is_match = match_col_in_line == Some(col); + let is_bracket = matches!( + ch, + '(' | ')' | '[' | ']' | '{' | '}' | '<' | '>' | '"' | '\'' + ); + + let style = if is_cursor && is_bracket && matching_bracket.is_some() { + Style::default() + .bg(Color::Rgb(100, 100, 50)) + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else if is_match { + Style::default() + .bg(Color::Rgb(50, 100, 50)) + .fg(Color::Green) + .add_modifier(Modifier::BOLD) + } else if is_bracket { + Style::default().fg(Color::Magenta) + } else if *ch == '"' || *ch == '\'' { + Style::default().fg(Color::Green) + } else if is_current_line { + Style::default().bg(Color::Rgb(60, 60, 60)).fg(Color::White) + } else { + Style::default().fg(Color::White) + }; + + spans.push(Span::styled(ch.to_string(), style)); + current_col += unicode_width::UnicodeWidthStr::width(ch.to_string().as_str()); + } + + let line_style = if is_current_line { + Style::default().bg(Color::Rgb(60, 60, 60)) + } else { + Style::default().bg(Color::Rgb(30, 30, 30)) + }; + + Line::from(spans).style(line_style) + }) + .collect(); + + let code_paragraph = Paragraph::new(code_lines) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().bg(Color::Rgb(30, 30, 30))); + + f.render_widget(code_paragraph, code_area); + + if state.cursor_line >= start_line && state.cursor_line < end_line { + let cursor_x = code_area.x + (state.cursor_col.saturating_sub(start_col)) as u16; + let cursor_y = code_area.y + (state.cursor_line - start_line) as u16; + if cursor_x < code_area.x + code_area.width { + use ratatui::layout::Position; + f.set_cursor_position(Position::new(cursor_x, cursor_y)); + } + } + } + + fn draw_welcome_screen(&mut self, f: &mut Frame, area: Rect) { + let welcome_text = vec![ + Line::from(vec![Span::styled( + "TermCode", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) + .add_modifier(Modifier::ITALIC), + )]), + Line::from(""), + Line::from(vec![Span::styled( + "Version ", + Style::default().fg(Color::Gray), + )]), + Line::from(vec![Span::styled( + "0.1.0", + Style::default().fg(Color::Green), + )]), + Line::from(""), + Line::from("File Explorer:"), + Line::from(" Tab - Switch between sidebar/editor"), + Line::from(" Arrow keys - Navigate files"), + Line::from(" Enter - Toggle folder / Open file"), + Line::from(" Enter on .. - Go up one directory"), + Line::from(" Ctrl+b - Toggle sidebar"), + Line::from(""), + Line::from("Commands:"), + Line::from(" Ctrl+p - Command palette"), + Line::from(" Ctrl+s - Save file"), + Line::from(" Ctrl+w - Close file"), + Line::from(" Ctrl+g - Go to line"), + Line::from(" Ctrl+f - Find in file"), + Line::from(" Alt+,/. - Previous/Next buffer"), + Line::from(" Esc - Cancel / Normal mode"), + ]; + + let paragraph = Paragraph::new(welcome_text) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::NONE) + .style(Style::default().bg(Color::Rgb(30, 30, 30))), + ) + .wrap(ratatui::widgets::Wrap { trim: true }); + + f.render_widget(paragraph, area); + } + + fn draw_status_bar(&mut self, f: &mut Frame, area: Rect, state: &AppState) { + let status_text = state.status_string(); + + let left_content = Line::from(vec![Span::styled( + format!(" {} ", status_text), + Style::default().bg(Color::Rgb(50, 50, 50)).fg(Color::White), + )]); + + let right_content = Line::from(vec![Span::styled( + format!(" {} ", chrono::Local::now().format("%H:%M")), + Style::default().bg(Color::Rgb(50, 50, 50)).fg(Color::Gray), + )]); + + // Add message if present + let mut lines = vec![left_content, right_content]; + + if let Some(ref message) = state.message { + let message_line = Line::from(vec![Span::styled( + format!(" {} ", message), + Style::default() + .bg(Color::Rgb(60, 60, 60)) + .fg(Color::Yellow), + )]); + lines.insert(0, message_line); + } + + let paragraph = Paragraph::new(lines) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().bg(Color::Rgb(50, 50, 50))); + + f.render_widget(paragraph, area); + } + + fn draw_command_palette(&mut self, f: &mut Frame, area: Rect) { + self.command_palette_items = vec![ + "Save File (Ctrl+s)".to_string(), + "Close File (Ctrl+w)".to_string(), + "Toggle Sidebar (Ctrl+b)".to_string(), + "Go to Line (Ctrl+g)".to_string(), + "Next Buffer (Alt+.)".to_string(), + "Previous Buffer (Alt+,)".to_string(), + "Quit (Ctrl+q)".to_string(), + ]; + + let palette_height = std::cmp::min(15, area.height as usize - 4); + let palette_area = Rect::new( + area.x + 2, + area.y + 2, + area.width - 4, + palette_height as u16, + ); + + let block = Block::default() + .title("Command Palette") + .borders(Borders::ALL) + .style(Style::default().bg(Color::Rgb(40, 40, 40))); + + f.render_widget(Clear, palette_area); + f.render_widget(block, palette_area); + + let list_items: Vec = self + .command_palette_items + .iter() + .enumerate() + .map(|(i, item)| { + let style = if i == self.command_palette_selected { + Style::default().bg(Color::Blue).fg(Color::White) + } else { + Style::default().fg(Color::White) + }; + let content = Line::from(vec![Span::styled(format!(" {}", item), style)]); + ListItem::new(content) + }) + .collect(); + + let list = List::new(list_items) + .block(Block::default().borders(Borders::NONE)) + .style(Style::default().bg(Color::Rgb(40, 40, 40))); + + let inner_area = Rect::new( + palette_area.x + 1, + palette_area.y + 2, + palette_area.width - 2, + palette_area.height - 3, + ); + + f.render_widget(list, inner_area); + } + + pub fn handle_key(&mut self, key: KeyEvent, state: &mut AppState) -> bool { + match state.input_mode { + crate::input::InputMode::Normal => self.handle_normal_mode(key, state), + crate::input::InputMode::Command => self.handle_command_mode(key, state), + crate::input::InputMode::GotoLine => self.handle_goto_mode(key, state), + _ => false, + } + } + + fn handle_normal_mode(&mut self, key: KeyEvent, state: &mut AppState) -> bool { + match (key.code, key.modifiers) { + (KeyCode::Char('c'), KeyModifiers::CONTROL) => { + state.set_input_mode(crate::input::InputMode::Command); + self.command_buffer.clear(); + true + } + (KeyCode::Esc, _) => { + state.set_input_mode(crate::input::InputMode::Normal); + true + } + _ => false, + } + } + + fn handle_command_mode(&mut self, key: KeyEvent, state: &mut AppState) -> bool { + match key.code { + KeyCode::Esc => { + state.set_input_mode(crate::input::InputMode::Normal); + self.command_buffer.clear(); + true + } + KeyCode::Enter => { + if !self.command_buffer.is_empty() { + state.add_command_to_history(self.command_buffer.clone()); + self.command_history.push(self.command_buffer.clone()); + state.set_message(format!("Command: {}", self.command_buffer)); + } + self.command_buffer.clear(); + state.set_input_mode(crate::input::InputMode::Normal); + true + } + KeyCode::Backspace => { + self.command_buffer.pop(); + true + } + KeyCode::Up => { + if let Some(cmd) = self.previous_command() { + self.command_buffer = cmd.clone(); + } + true + } + KeyCode::Down => { + if let Some(cmd) = self.next_command() { + self.command_buffer = cmd.clone(); + } + true + } + KeyCode::Char(ch) => { + self.command_buffer.push(ch); + true + } + _ => false, + } + } + + fn handle_goto_mode(&mut self, key: KeyEvent, state: &mut AppState) -> bool { + match key.code { + KeyCode::Esc => { + state.set_input_mode(crate::input::InputMode::Normal); + self.goto_buffer.clear(); + true + } + KeyCode::Enter => { + if let Ok(line) = self.goto_buffer.parse::() { + let line_num = line.saturating_sub(1); + state.set_cursor_position(line_num, 0); + state.set_message(format!("Going to line {}", line)); + } + self.goto_buffer.clear(); + state.set_input_mode(crate::input::InputMode::Normal); + true // Return true to signal that cursor position changed + } + KeyCode::Backspace => { + self.goto_buffer.pop(); + false + } + KeyCode::Char(ch) if ch.is_ascii_digit() => { + self.goto_buffer.push(ch); + false + } + _ => false, + } + } + + fn previous_command(&mut self) -> Option<&String> { + if !self.command_history.is_empty() && self.command_history_index > 0 { + self.command_history_index -= 1; + return self.command_history.get(self.command_history_index); + } + None + } + + fn next_command(&mut self) -> Option<&String> { + if self.command_history_index < self.command_history.len() { + self.command_history_index += 1; + return self + .command_history + .get(self.command_history_index.saturating_sub(1)); + } + None + } + + pub fn is_selected_go_up(&self) -> bool { + self.can_go_up && self.sidebar_selected == 0 + } + + pub fn get_selected_command(&self) -> Option<&String> { + self.command_palette_items + .get(self.command_palette_selected) + } +} diff --git a/termcode.svg b/termcode.svg new file mode 100644 index 0000000..1d9aeda --- /dev/null +++ b/termcode.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +