From 0a95bae451c1847f4f0b9601e60959f4e8e6b669 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Thu, 12 Mar 2026 14:28:08 +0800 Subject: Refactor display utilities --- Cargo.lock | 316 ++++++++++---------- Cargo.toml | 73 ++--- macros/cmd_system_macros/Cargo.toml | 6 +- macros/render_system_macros/Cargo.toml | 6 +- src/bin/jv.rs | 10 +- src/bin/jvii.rs | 6 +- src/bin/jvn.rs | 4 +- src/bin/jvv.rs | 2 +- src/cmds/arg/storage_build.rs | 12 - src/cmds/arg/storage_write.rs | 33 --- src/cmds/cmd/sheetedit.rs | 10 +- src/cmds/cmd/storage_build.rs | 70 ----- src/cmds/cmd/storage_write.rs | 110 ------- src/cmds/in/storage_rw.rs | 11 - src/cmds/renderer/mappings_pretty.rs | 8 +- tools/build_helper/Cargo.toml | 9 +- utils/Cargo.toml | 24 +- utils/src/display.rs | 490 +------------------------------ utils/src/display/colorful.rs | 275 +++++++++++++++++ utils/src/display/table.rs | 143 +++++++++ utils/src/env.rs | 107 ------- utils/src/fs.rs | 40 --- utils/src/globber.rs | 279 ------------------ utils/src/input.rs | 151 ---------- utils/src/lazy_macros.rs | 14 - utils/src/legacy.rs | 9 + utils/src/legacy/display.rs | 488 ++++++++++++++++++++++++++++++ utils/src/legacy/env.rs | 107 +++++++ utils/src/legacy/fs.rs | 40 +++ utils/src/legacy/globber.rs | 279 ++++++++++++++++++ utils/src/legacy/input.rs | 151 ++++++++++ utils/src/legacy/levenshtein_distance.rs | 34 +++ utils/src/legacy/logger.rs | 86 ++++++ utils/src/legacy/push_version.rs | 30 ++ utils/src/legacy/socket_addr_helper.rs | 194 ++++++++++++ utils/src/levenshtein_distance.rs | 34 --- utils/src/lib.rs | 14 +- utils/src/logger.rs | 86 ------ utils/src/macros.rs | 14 + utils/src/math.rs | 1 + utils/src/math/levenshtein_distance.rs | 38 +++ utils/src/push_version.rs | 30 -- utils/src/socket_addr_helper.rs | 194 ------------ 43 files changed, 2120 insertions(+), 1918 deletions(-) delete mode 100644 src/cmds/arg/storage_build.rs delete mode 100644 src/cmds/arg/storage_write.rs delete mode 100644 src/cmds/cmd/storage_build.rs delete mode 100644 src/cmds/cmd/storage_write.rs delete mode 100644 src/cmds/in/storage_rw.rs create mode 100644 utils/src/display/colorful.rs create mode 100644 utils/src/display/table.rs delete mode 100644 utils/src/env.rs delete mode 100644 utils/src/fs.rs delete mode 100644 utils/src/globber.rs delete mode 100644 utils/src/input.rs delete mode 100644 utils/src/lazy_macros.rs create mode 100644 utils/src/legacy.rs create mode 100644 utils/src/legacy/display.rs create mode 100644 utils/src/legacy/env.rs create mode 100644 utils/src/legacy/fs.rs create mode 100644 utils/src/legacy/globber.rs create mode 100644 utils/src/legacy/input.rs create mode 100644 utils/src/legacy/levenshtein_distance.rs create mode 100644 utils/src/legacy/logger.rs create mode 100644 utils/src/legacy/push_version.rs create mode 100644 utils/src/legacy/socket_addr_helper.rs delete mode 100644 utils/src/levenshtein_distance.rs delete mode 100644 utils/src/logger.rs create mode 100644 utils/src/macros.rs create mode 100644 utils/src/math.rs create mode 100644 utils/src/math/levenshtein_distance.rs delete mode 100644 utils/src/push_version.rs delete mode 100644 utils/src/socket_addr_helper.rs diff --git a/Cargo.lock b/Cargo.lock index 0224d56..afc5e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,7 +159,7 @@ dependencies = [ "asset_macros", "constants", "just_fmt", - "thiserror 1.0.69", + "thiserror", "tokio", "winapi", ] @@ -429,6 +429,7 @@ version = "0.1.0-dev" dependencies = [ "chrono", "colored", + "crossterm", "dirs", "env_logger", "just_enough_vcs", @@ -455,11 +456,22 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" -version = "3.0.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "config_system" +version = "0.1.0" dependencies = [ - "windows-sys 0.59.0", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "toml 0.9.8", ] [[package]] @@ -493,6 +505,15 @@ dependencies = [ "syn", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -559,15 +580,17 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crossterm" -version = "0.27.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ "bitflags 2.9.4", "crossterm_winapi", - "libc", - "mio 0.8.11", + "derive_more", + "document-features", + "mio", "parking_lot", + "rustix", "signal-hook", "signal-hook-mio", "winapi", @@ -647,6 +670,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -690,6 +735,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + [[package]] name = "ed25519" version = "3.0.0-rc.1" @@ -748,14 +802,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "erased-serde" -version = "0.4.9" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "serde", - "serde_core", - "typeid", + "libc", + "windows-sys 0.61.2", ] [[package]] @@ -776,6 +829,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "framework" +version = "0.1.0" +dependencies = [ + "just_fmt", + "space_macro", + "thiserror", + "tokio", +] + [[package]] name = "futures" version = "0.3.31" @@ -963,12 +1026,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "hex_display" version = "0.1.0" @@ -1122,18 +1179,21 @@ dependencies = [ "asset_system", "cfg_file", "chrono", + "config_system", "constants", "data_struct", + "framework", "hex_display", "jvlib", "sha1_hash", "sheet_system", - "storage_system", "tcp_connection", "toml 0.9.8", + "vault_system", "vcs_actions", "vcs_data", "vcs_docs", + "workspace_system", ] [[package]] @@ -1147,7 +1207,6 @@ dependencies = [ "colored", "crossterm", "env_logger", - "erased-serde", "just_enough_vcs", "just_fmt", "just_progress", @@ -1160,7 +1219,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "thiserror 2.0.17", + "thiserror", "tokio", "toml 0.9.8", "walkdir", @@ -1236,6 +1295,18 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + [[package]] name = "lock_api" version = "0.4.14" @@ -1266,18 +1337,6 @@ dependencies = [ "libc", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.1.0" @@ -1285,6 +1344,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] @@ -1621,7 +1681,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.17", + "thiserror", ] [[package]] @@ -1795,6 +1855,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1976,7 +2049,7 @@ dependencies = [ "serde", "sha2 0.10.9", "sheet_system_macros", - "thiserror 1.0.69", + "thiserror", "tokio", ] @@ -2012,7 +2085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", - "mio 0.8.11", + "mio", "signal-hook", ] @@ -2069,6 +2142,16 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "space_macro" +version = "0.1.0" +dependencies = [ + "just_fmt", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "spin" version = "0.9.8" @@ -2091,18 +2174,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "storage_system" -version = "0.1.0" -dependencies = [ - "blake3", - "hex", - "log", - "memmap2", - "thiserror 1.0.69", - "tokio", -] - [[package]] name = "strip-ansi-escapes" version = "0.2.1" @@ -2150,38 +2221,18 @@ dependencies = [ "rsa", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror", "tokio", "uuid", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.17", -] - -[[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", + "thiserror-impl", ] [[package]] @@ -2197,13 +2248,13 @@ dependencies = [ [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", - "mio 1.1.0", + "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -2314,12 +2365,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "typeid" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" - [[package]] name = "typenum" version = "1.19.0" @@ -2332,6 +2377,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2368,6 +2419,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vault_system" +version = "0.1.0" +dependencies = [ + "asset_system", + "config_system", + "constants", + "framework", + "serde", + "thiserror", + "tokio", +] + [[package]] name = "vcs_actions" version = "0.1.0" @@ -2380,7 +2444,7 @@ dependencies = [ "serde_json", "sha1_hash", "tcp_connection", - "thiserror 2.0.17", + "thiserror", "tokio", "vcs_data", ] @@ -2670,15 +2734,6 @@ 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.52.0" @@ -2688,15 +2743,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[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" @@ -2715,21 +2761,6 @@ 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" @@ -2763,12 +2794,6 @@ dependencies = [ "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" @@ -2781,12 +2806,6 @@ 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" @@ -2799,12 +2818,6 @@ 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" @@ -2829,12 +2842,6 @@ 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" @@ -2847,12 +2854,6 @@ 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" @@ -2865,12 +2866,6 @@ 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" @@ -2883,12 +2878,6 @@ 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" @@ -3004,6 +2993,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "workspace_system" +version = "0.1.0" +dependencies = [ + "asset_system", + "config_system", + "constants", + "framework", + "serde", + "thiserror", + "tokio", +] + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/Cargo.toml b/Cargo.toml index 2b2aa7e..e6c061f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ members = [ "utils/", "tools/build_helper", "macros/render_system_macros", - "macros/cmd_system_macros" + "macros/cmd_system_macros", ] [workspace.package] @@ -44,56 +44,39 @@ toml = "0.9" regex = "1.12" just_template = "0.1.0" +[workspace.dependencies] +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "2.0", features = ["full", "extra-traits"] } +just_fmt = "0.1.2" +crossterm = "0.29" +colored = "3.1" +log = "0.4" +env_logger = "0.11" +thiserror = "2" +serde = { version = "1", features = ["derive"] } + [dependencies] -# Just Enough VCS +cli_utils = { path = "utils" } +cmd_system_macros = { path = "macros/cmd_system_macros" } just_enough_vcs = { path = "../VersionControl", features = ["all"] } - -# RenderSystem Macros render_system_macros = { path = "macros/render_system_macros" } -# CommandSystem Macros -cmd_system_macros = { path = "macros/cmd_system_macros" } +crossterm.workspace = true +env_logger.workspace = true +just_fmt.workspace = true +log.workspace = true +serde.workspace = true +thiserror.workspace = true -# CommandLine Utilities -cli_utils = { path = "utils" } - -# Error -thiserror = "2.0.17" - -# Serialize -# What the heck, why does this crate use kebab-case instead of snake_case ???? -erased_serde = { package = "erased-serde", version = "0.4" } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -serde_yaml = "0.9" -ron = "0.11.0" -toml = "0.9" - -# Command Line -clap = { version = "4.5", features = ["derive"] } - -# Time chrono = "0.4" - -# Logging -log = "0.4" -env_logger = "0.11" - -# Async -tokio = { version = "1", features = ["full"] } - -# Display -colored = "3.0" +clap = { version = "4.5", features = ["derive"] } +colored.workspace = true just_progress = "0.1.1" - -# Terminal -crossterm = "0.27" - -# i18n +ron = "0.11.0" rust-i18n = "3" - -# File & Directory +serde_json = "1" +serde_yaml = "0.9" +tokio = { version = "1", features = ["full"] } +toml = "0.9" walkdir = "2.5.0" - -# String format -just_fmt = "0.1.2" diff --git a/macros/cmd_system_macros/Cargo.toml b/macros/cmd_system_macros/Cargo.toml index 4a91064..3f9e0b7 100644 --- a/macros/cmd_system_macros/Cargo.toml +++ b/macros/cmd_system_macros/Cargo.toml @@ -7,6 +7,6 @@ edition = "2024" proc-macro = true [dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full", "extra-traits", "visit"] } +proc-macro2.workspace = true +quote.workspace = true +syn = { workspace = true, features = ["visit"] } diff --git a/macros/render_system_macros/Cargo.toml b/macros/render_system_macros/Cargo.toml index df435db..28f084d 100644 --- a/macros/render_system_macros/Cargo.toml +++ b/macros/render_system_macros/Cargo.toml @@ -7,6 +7,6 @@ edition = "2024" proc-macro = true [dependencies] -proc-macro2 = "1.0" -quote = "1.0" -syn = { version = "2.0", features = ["full", "extra-traits"] } +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/src/bin/jv.rs b/src/bin/jv.rs index c3dc4ca..0df6b32 100644 --- a/src/bin/jv.rs +++ b/src/bin/jv.rs @@ -21,7 +21,6 @@ // The new implementation is located in `jvn.rs`, please refer to it. // -use cli_utils::input::input_with_editor; use colored::Colorize; use just_enough_vcs::{ data::compile_info::CoreCompileInfo, @@ -113,14 +112,13 @@ use std::{ }; use clap::{Parser, Subcommand}; -use cli_utils::{ +use cli_utils::legacy::{ display::{SimpleTable, display_width, md, render_share_path_tree, size_str}, env::{auto_update_outdate, current_locales, enable_auto_update}, fs::move_across_partitions, globber::{GlobItem, Globber}, - input::{confirm_hint, confirm_hint_or, show_in_pager}, - push_version::push_version, - socket_addr_helper, + input::{confirm_hint, confirm_hint_or, input_with_editor, show_in_pager}, + push_version, socket_addr_helper, }; use just_enough_vcs::utils::tcp_connection::error::TcpTargetError; use just_enough_vcs_cli::{ @@ -4035,7 +4033,7 @@ async fn start_update_editor( for item in files { let path = item.0.display().to_string(); let base_ver = item.1.to_string(); - let next_ver = push_version(&base_ver).unwrap_or(" ".to_string()); + let next_ver = push_version::push_version(&base_ver).unwrap_or(" ".to_string()); table.push_item(vec![ path, base_ver, diff --git a/src/bin/jvii.rs b/src/bin/jvii.rs index 2f9cb6d..fbc3ed9 100644 --- a/src/bin/jvii.rs +++ b/src/bin/jvii.rs @@ -5,9 +5,9 @@ use std::path::PathBuf; use std::time::Duration; use clap::Parser; -use cli_utils::display::display_width; -use cli_utils::display::md; -use cli_utils::env::current_locales; +use cli_utils::legacy::display::display_width; +use cli_utils::legacy::display::md; +use cli_utils::legacy::env::current_locales; use crossterm::{ QueueableCommand, cursor::MoveTo, diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs index f2c5cf6..f45c78b 100644 --- a/src/bin/jvn.rs +++ b/src/bin/jvn.rs @@ -1,6 +1,6 @@ use std::{ops::Deref, process::exit}; -use cli_utils::{display::md, env::current_locales, levenshtein_distance::levenshtein_distance}; +use cli_utils::legacy::{display::md, env::current_locales, levenshtein_distance}; use just_enough_vcs_cli::{ special_argument, special_flag, systems::{ @@ -197,7 +197,7 @@ fn handle_no_matching_command_error(args: Vec) { continue; } let args_str = args[..node_len].join(" "); - let distance = levenshtein_distance(args_str.as_str(), node.as_str()); + let distance = levenshtein_distance::levenshtein_distance(args_str.as_str(), node.as_str()); if distance <= 2 { similar_nodes.push(node); } diff --git a/src/bin/jvv.rs b/src/bin/jvv.rs index 335e374..885f3e0 100644 --- a/src/bin/jvv.rs +++ b/src/bin/jvv.rs @@ -22,7 +22,7 @@ // use clap::{Parser, Subcommand}; -use cli_utils::{ +use cli_utils::legacy::{ display::{md, size_str}, env::current_locales, logger::build_env_logger, diff --git a/src/cmds/arg/storage_build.rs b/src/cmds/arg/storage_build.rs deleted file mode 100644 index 5d57b97..0000000 --- a/src/cmds/arg/storage_build.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; - -#[derive(Parser, Debug)] -pub struct JVStorageBuildArgument { - pub index_file: PathBuf, - pub storage: PathBuf, - - #[arg(short = 'o', long = "output")] - pub output_file: Option, -} diff --git a/src/cmds/arg/storage_write.rs b/src/cmds/arg/storage_write.rs deleted file mode 100644 index e00dfdf..0000000 --- a/src/cmds/arg/storage_write.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; - -#[derive(Parser, Debug)] -pub struct JVStorageWriteArgument { - pub file: PathBuf, - pub storage: PathBuf, - - #[arg(short = 'o', long = "output")] - pub output_index: Option, - - #[arg(long = "line")] - pub line_chunking: bool, - - #[arg(long = "cdc", default_value_t = 0)] - pub cdc_chunking: u32, - - #[arg(long = "fixed", default_value_t = 0)] - pub fixed_chunking: u32, - - #[arg(long)] - pub b: bool, - - #[arg(long)] - pub kb: bool, // default chunk size unit - - #[arg(long)] - pub mb: bool, - - #[arg(long)] - pub gb: bool, -} diff --git a/src/cmds/cmd/sheetedit.rs b/src/cmds/cmd/sheetedit.rs index 3e61642..74b0bbe 100644 --- a/src/cmds/cmd/sheetedit.rs +++ b/src/cmds/cmd/sheetedit.rs @@ -10,7 +10,9 @@ use crate::{ }, }; use cli_utils::{ - display::SimpleTable, env::get_default_editor, input::input_with_editor_cutsom, string_vec, + display::table::Table, + legacy::{env::get_default_editor, input::input_with_editor_cutsom}, + string_vec, }; use cmd_system_macros::exec; use just_enough_vcs::system::sheet_system::{mapping::LocalMapping, sheet::SheetData}; @@ -96,7 +98,7 @@ fn render_pretty_mappings(mappings: &Vec) -> String { t!("sheetedit.forward") ]; - let mut simple_table = SimpleTable::new(header); + let mut table = Table::new(header); for mapping in mappings { let mapping_str = mapping @@ -105,7 +107,7 @@ fn render_pretty_mappings(mappings: &Vec) -> String { .into_iter() .map(|s| s.to_string()) .collect::>(); - simple_table.push_item(vec![ + table.push_item(vec![ format!( " {} ", mapping_str.get(0).unwrap_or(&String::default()) @@ -116,7 +118,7 @@ fn render_pretty_mappings(mappings: &Vec) -> String { format!("{} ", mapping_str.get(4).unwrap_or(&String::default())), // Forward ]); } - simple_table.to_string() + table.to_string() } crate::command_template!(); diff --git a/src/cmds/cmd/storage_build.rs b/src/cmds/cmd/storage_build.rs deleted file mode 100644 index 8e4d39c..0000000 --- a/src/cmds/cmd/storage_build.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - cmd_output, - cmds::{ - arg::storage_build::JVStorageBuildArgument, collect::empty::JVEmptyCollect, - r#in::storage_rw::JVStorageRWInput, out::none::JVNoneOutput, - }, - systems::cmd::{ - cmd_system::JVCommandContext, - errors::{CmdExecuteError, CmdPrepareError}, - }, -}; -use cli_utils::display::md; -use cmd_system_macros::exec; -use just_enough_vcs::system::storage_system::{error::StorageIOError, store::build_file}; -use rust_i18n::t; -use std::any::TypeId; - -pub struct JVStorageBuildCommand; -type Cmd = JVStorageBuildCommand; -type Arg = JVStorageBuildArgument; -type In = JVStorageRWInput; -type Collect = JVEmptyCollect; - -fn help_str() -> String { - todo!() -} - -async fn prepare(args: &Arg, _ctx: &JVCommandContext) -> Result { - let output_file = match &args.output_file { - Some(v) => v.clone(), - None => args.index_file.clone().with_extension("unknown"), - }; - - let (input, storage, output) = just_enough_vcs::system::storage_system::store::precheck( - args.index_file.clone(), - args.storage.clone(), - output_file, - ) - .await?; - - Ok(JVStorageRWInput { - input, - storage, - output, - chunking_policy: None, - }) -} - -async fn collect(_args: &Arg, _ctx: &JVCommandContext) -> Result { - Ok(Collect {}) -} - -#[exec] -async fn exec( - input: In, - _collect: Collect, -) -> Result<(Box, TypeId), CmdExecuteError> { - build_file(input.input, input.storage, input.output) - .await - .map_err(|e| match e { - StorageIOError::IOErr(error) => CmdExecuteError::Io(error), - StorageIOError::HashTooShort => { - CmdExecuteError::Error(md(t!("storage_write.hash_too_short")).to_string()) - } - })?; - - cmd_output!(JVNoneOutput => JVNoneOutput {}) -} - -crate::command_template!(); diff --git a/src/cmds/cmd/storage_write.rs b/src/cmds/cmd/storage_write.rs deleted file mode 100644 index 8c864a8..0000000 --- a/src/cmds/cmd/storage_write.rs +++ /dev/null @@ -1,110 +0,0 @@ -use crate::{ - cmd_output, - cmds::{ - arg::storage_write::JVStorageWriteArgument, collect::empty::JVEmptyCollect, - r#in::storage_rw::JVStorageRWInput, out::none::JVNoneOutput, - }, - systems::cmd::{ - cmd_system::JVCommandContext, - errors::{CmdExecuteError, CmdPrepareError}, - }, -}; -use cli_utils::display::md; -use cmd_system_macros::exec; -use just_enough_vcs::system::{ - constants::vault::values::vault_value_index_file_suffix, - storage_system::{ - error::StorageIOError, - store::{ChunkingPolicy, StorageConfig, write_file}, - }, -}; -use rust_i18n::t; -use std::any::TypeId; - -pub struct JVStorageWriteCommand; -type Cmd = JVStorageWriteCommand; -type Arg = JVStorageWriteArgument; -type In = JVStorageRWInput; -type Collect = JVEmptyCollect; - -fn help_str() -> String { - todo!() -} - -async fn prepare(args: &Arg, _ctx: &JVCommandContext) -> Result { - let output_path = match &args.output_index { - Some(v) => v.clone(), - None => args - .file - .clone() - .with_extension(vault_value_index_file_suffix()), - }; - - // Default to using kb as the unit - let scale = if args.gb { - 1024 * 1024 * 1024 - } else if args.mb { - 1024 * 1024 - } else if args.b { - 1 - } else { - 1024 - }; - - let (input, storage, output) = just_enough_vcs::system::storage_system::store::precheck( - args.file.clone(), - args.storage.clone(), - output_path, - ) - .await?; - - let chunking_policy: ChunkingPolicy = if args.cdc_chunking > 0 { - ChunkingPolicy::Cdc(args.cdc_chunking * scale) - } else if args.fixed_chunking > 0 { - ChunkingPolicy::FixedSize(args.fixed_chunking * scale) - } else if args.line_chunking { - ChunkingPolicy::Line - } else { - return Err(CmdPrepareError::Error(md(t!( - "storage_write.unknown_chunking_policy" - )))); - }; - - Ok(JVStorageRWInput { - input, - storage, - output, - chunking_policy: Some(chunking_policy), - }) -} - -async fn collect(_args: &Arg, _ctx: &JVCommandContext) -> Result { - Ok(JVEmptyCollect {}) -} - -#[exec] -async fn exec( - input: In, - _collect: Collect, -) -> Result<(Box, TypeId), CmdExecuteError> { - // There is no chance to return None in the Prepare phase, so unwrap is safe here - let chunking_policy = input.chunking_policy.unwrap(); - - write_file( - input.input, - input.storage, - input.output, - &StorageConfig { chunking_policy }, - ) - .await - .map_err(|e| match e { - StorageIOError::IOErr(error) => CmdExecuteError::Io(error), - StorageIOError::HashTooShort => { - CmdExecuteError::Error(md(t!("storage_write.hash_too_short")).to_string()) - } - })?; - - cmd_output!(JVNoneOutput => JVNoneOutput {}) -} - -crate::command_template!(); diff --git a/src/cmds/in/storage_rw.rs b/src/cmds/in/storage_rw.rs deleted file mode 100644 index 596c1f9..0000000 --- a/src/cmds/in/storage_rw.rs +++ /dev/null @@ -1,11 +0,0 @@ -use std::path::PathBuf; - -use just_enough_vcs::system::storage_system::store::ChunkingPolicy; - -pub struct JVStorageRWInput { - pub input: PathBuf, - pub storage: PathBuf, - pub output: PathBuf, - - pub chunking_policy: Option, -} diff --git a/src/cmds/renderer/mappings_pretty.rs b/src/cmds/renderer/mappings_pretty.rs index dad4d95..6431302 100644 --- a/src/cmds/renderer/mappings_pretty.rs +++ b/src/cmds/renderer/mappings_pretty.rs @@ -1,4 +1,4 @@ -use cli_utils::{display::SimpleTable, string_vec}; +use cli_utils::{display::table::Table, string_vec}; use colored::Colorize; use just_enough_vcs::system::sheet_system::mapping::LocalMapping; use render_system_macros::result_renderer; @@ -29,7 +29,7 @@ fn render_pretty_mappings(mappings: &Vec) -> String { "|" ]; - let mut simple_table = SimpleTable::new(header); + let mut table = Table::new(header); let mut i = 1; for mapping in mappings { @@ -39,7 +39,7 @@ fn render_pretty_mappings(mappings: &Vec) -> String { .into_iter() .map(|s| s.to_string()) .collect::>(); - simple_table.push_item(vec![ + table.push_item(vec![ // Number format!("{}", i).bold().to_string(), // Mapping @@ -89,5 +89,5 @@ fn render_pretty_mappings(mappings: &Vec) -> String { i += 1; } - simple_table.to_string() + table.to_string() } diff --git a/tools/build_helper/Cargo.toml b/tools/build_helper/Cargo.toml index deadd4c..7682795 100644 --- a/tools/build_helper/Cargo.toml +++ b/tools/build_helper/Cargo.toml @@ -4,10 +4,7 @@ edition = "2024" version = "0.0.1" [dependencies] +colored.workspace = true +serde.workspace = true -# Serialization -serde = { version = "1.0.226", features = ["derive"] } -toml = { version = "0.9.7", features = ["serde"] } - -# Colored -colored = "3.0.0" +toml = { version = "0.9", features = ["serde"] } diff --git a/utils/Cargo.toml b/utils/Cargo.toml index c055c07..e4cc3a0 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -4,23 +4,15 @@ edition = "2024" version.workspace = true [dependencies] -# Just Enough VCS just_enough_vcs = { path = "../../VersionControl", features = ["all"] } -# Display -colored = "3.0" -strip-ansi-escapes = "0.2.1" -just_fmt = "0.1.2" - -# Async -tokio = { version = "1", features = ["full"] } +colored.workspace = true +crossterm.workspace = true +env_logger.workspace = true +just_fmt.workspace = true +log.workspace = true -# Logging -log = "0.4" -env_logger = "0.11" - -# File & Directory -dirs = "6.0.0" - -# Time chrono = "0.4" +dirs = "6.0.0" +strip-ansi-escapes = "0.2.1" +tokio = { version = "1", features = ["fs", "io-std", "net"] } diff --git a/utils/src/display.rs b/utils/src/display.rs index fc94d90..a9c48e8 100644 --- a/utils/src/display.rs +++ b/utils/src/display.rs @@ -1,488 +1,2 @@ -use colored::*; -use just_enough_vcs::lib::data::sheet::SheetMappingMetadata; -use std::{ - collections::{BTreeMap, HashMap, VecDeque}, - path::PathBuf, -}; - -pub struct SimpleTable { - items: Vec, - line: Vec>, - length: Vec, - padding: usize, -} - -impl SimpleTable { - /// Create a new Table - pub fn new(items: Vec>) -> Self { - Self::new_with_padding(items, 2) - } - - /// Create a new Table with padding - pub fn new_with_padding(items: Vec>, padding: usize) -> Self { - let items: Vec = items.into_iter().map(|v| v.into()).collect(); - let mut length = Vec::with_capacity(items.len()); - - for item in &items { - length.push(display_width(item)); - } - - SimpleTable { - items, - padding, - line: Vec::new(), - length, - } - } - - /// Push a new row of items to the table - pub fn push_item(&mut self, items: Vec>) { - let items: Vec = items.into_iter().map(|v| v.into()).collect(); - - let mut processed_items = Vec::with_capacity(self.items.len()); - - for i in 0..self.items.len() { - if i < items.len() { - processed_items.push(items[i].clone()); - } else { - processed_items.push(String::new()); - } - } - - for (i, d) in processed_items.iter().enumerate() { - let d_len = display_width(d); - if d_len > self.length[i] { - self.length[i] = d_len; - } - } - - self.line.push(processed_items); - } - - /// Insert a new row of items at the specified index - pub fn insert_item(&mut self, index: usize, items: Vec>) { - let items: Vec = items.into_iter().map(|v| v.into()).collect(); - - let mut processed_items = Vec::with_capacity(self.items.len()); - - for i in 0..self.items.len() { - if i < items.len() { - processed_items.push(items[i].clone()); - } else { - processed_items.push(String::new()); - } - } - - for (i, d) in processed_items.iter().enumerate() { - let d_len = display_width(d); - if d_len > self.length[i] { - self.length[i] = d_len; - } - } - - self.line.insert(index, processed_items); - } - - /// Get the current maximum column widths - fn get_column_widths(&self) -> &[usize] { - &self.length - } -} - -impl std::fmt::Display for SimpleTable { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let column_widths = self.get_column_widths(); - - // Build the header row - let header: Vec = self - .items - .iter() - .enumerate() - .map(|(i, item)| { - let target_width = column_widths[i] + self.padding; - let current_width = display_width(item); - let space_count = target_width - current_width; - let space = " ".repeat(space_count); - let result = format!("{}{}", item, space); - result - }) - .collect(); - writeln!(f, "{}", header.join(""))?; - - // Build each data row - for row in &self.line { - let formatted_row: Vec = row - .iter() - .enumerate() - .map(|(i, cell)| { - let target_width = column_widths[i] + self.padding; - let current_width = display_width(cell); - let space_count = target_width - current_width; - let spaces = " ".repeat(space_count); - let result = format!("{}{}", cell, spaces); - result - }) - .collect(); - writeln!(f, "{}", formatted_row.join(""))?; - } - - Ok(()) - } -} - -pub fn display_width(s: &str) -> usize { - // Filter out ANSI escape sequences before calculating width - let filtered_bytes = strip_ansi_escapes::strip(s); - let filtered_str = match std::str::from_utf8(&filtered_bytes) { - Ok(s) => s, - Err(_) => s, // Fallback to original string if UTF-8 conversion fails - }; - - let mut width = 0; - for c in filtered_str.chars() { - if c.is_ascii() { - width += 1; - } else { - width += 2; - } - } - width -} - -/// Convert byte size to a human-readable string format -/// -/// Automatically selects the appropriate unit (B, KB, MB, GB, TB) based on the byte size -/// and formats it as a string with two decimal places -pub fn size_str(total_size: usize) -> String { - if total_size < 1024 { - format!("{} B", total_size) - } else if total_size < 1024 * 1024 { - format!("{:.2} KB", total_size as f64 / 1024.0) - } else if total_size < 1024 * 1024 * 1024 { - format!("{:.2} MB", total_size as f64 / (1024.0 * 1024.0)) - } else if total_size < 1024 * 1024 * 1024 * 1024 { - format!("{:.2} GB", total_size as f64 / (1024.0 * 1024.0 * 1024.0)) - } else { - format!( - "{:.2} TB", - total_size as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0) - ) - } -} - -// Convert the Markdown formatted text into a format supported by the command line -pub fn md(text: impl AsRef) -> String { - let text = text.as_ref().trim(); - let mut result = String::new(); - let mut color_stack: VecDeque = VecDeque::new(); - - let mut i = 0; - let chars: Vec = text.chars().collect(); - - while i < chars.len() { - // Check for escape character \ - if chars[i] == '\\' && i + 1 < chars.len() { - let escaped_char = chars[i + 1]; - // Only escape specific characters - if matches!(escaped_char, '*' | '<' | '>' | '`') { - let mut escaped_text = escaped_char.to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - escaped_text = apply_color(&escaped_text, color); - } - - result.push_str(&escaped_text); - i += 2; - continue; - } - } - - // Check for color tag start [[color]] - if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' { - let mut j = i + 2; - while j < chars.len() - && !(chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == ']') - { - j += 1; - } - - if j + 1 < chars.len() { - let tag_content: String = chars[i + 2..j].iter().collect(); - - // Check if it's a closing tag [[/]] - if tag_content == "/" { - color_stack.pop_back(); - i = j + 2; - continue; - } - - // It's a color tag - color_stack.push_back(tag_content.clone()); - i = j + 2; - continue; - } - } - - // Check for bold **text** - if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { - let mut j = i + 2; - while j + 1 < chars.len() && !(chars[j] == '*' && chars[j + 1] == '*') { - j += 1; - } - - if j + 1 < chars.len() { - let bold_text: String = chars[i + 2..j].iter().collect(); - let mut formatted_text = bold_text.bold().to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - formatted_text = apply_color(&formatted_text, color); - } - - result.push_str(&formatted_text); - i = j + 2; - continue; - } - } - - // Check for italic *text* - if chars[i] == '*' { - let mut j = i + 1; - while j < chars.len() && chars[j] != '*' { - j += 1; - } - - if j < chars.len() { - let italic_text: String = chars[i + 1..j].iter().collect(); - let mut formatted_text = italic_text.italic().to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - formatted_text = apply_color(&formatted_text, color); - } - - result.push_str(&formatted_text); - i = j + 1; - continue; - } - } - - // Check for angle-bracketed content - if chars[i] == '<' { - let mut j = i + 1; - while j < chars.len() && chars[j] != '>' { - j += 1; - } - - if j < chars.len() { - // Include the angle brackets in the output - let angle_text: String = chars[i..=j].iter().collect(); - let mut formatted_text = angle_text.cyan().to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - formatted_text = apply_color(&formatted_text, color); - } - - result.push_str(&formatted_text); - i = j + 1; - continue; - } - } - - // Check for inline code `text` - if chars[i] == '`' { - let mut j = i + 1; - while j < chars.len() && chars[j] != '`' { - j += 1; - } - - if j < chars.len() { - // Include the backticks in the output - let code_text: String = chars[i..=j].iter().collect(); - let mut formatted_text = code_text.green().to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - formatted_text = apply_color(&formatted_text, color); - } - - result.push_str(&formatted_text); - i = j + 1; - continue; - } - } - - // Regular character - let mut current_char = chars[i].to_string(); - - // Apply current color stack - for color in color_stack.iter().rev() { - current_char = apply_color(¤t_char, color); - } - - result.push_str(¤t_char); - i += 1; - } - - result -} - -// Helper function to apply color to text -fn apply_color(text: impl AsRef, color_name: impl AsRef) -> String { - let text = text.as_ref(); - let color_name = color_name.as_ref(); - match color_name { - // Normal colors - "black" => text.black().to_string(), - "red" => text.red().to_string(), - "green" => text.green().to_string(), - "yellow" => text.yellow().to_string(), - "blue" => text.blue().to_string(), - "magenta" => text.magenta().to_string(), - "cyan" => text.cyan().to_string(), - "white" => text.white().to_string(), - "bright_black" => text.bright_black().to_string(), - "bright_red" => text.bright_red().to_string(), - "bright_green" => text.bright_green().to_string(), - "bright_yellow" => text.bright_yellow().to_string(), - "bright_blue" => text.bright_blue().to_string(), - "bright_magenta" => text.bright_magenta().to_string(), - "bright_cyan" => text.bright_cyan().to_string(), - "bright_white" => text.bright_white().to_string(), - - // Short aliases for bright colors - "b_black" => text.bright_black().to_string(), - "b_red" => text.bright_red().to_string(), - "b_green" => text.bright_green().to_string(), - "b_yellow" => text.bright_yellow().to_string(), - "b_blue" => text.bright_blue().to_string(), - "b_magenta" => text.bright_magenta().to_string(), - "b_cyan" => text.bright_cyan().to_string(), - "b_white" => text.bright_white().to_string(), - - // Gray colors using truecolor - "gray" | "grey" => text.truecolor(128, 128, 128).to_string(), - "bright_gray" | "bright_grey" => text.truecolor(192, 192, 192).to_string(), - "b_gray" | "b_grey" => text.truecolor(192, 192, 192).to_string(), - - // Default to white if color not recognized - _ => text.to_string(), - } -} - -/// Render a HashMap of PathBuf to SheetMappingMetadata as a tree string. -pub fn render_share_path_tree(paths: &HashMap) -> String { - if paths.is_empty() { - return String::new(); - } - - // Collect all path components into a tree structure - let mut root = TreeNode::new("".to_string()); - - for (path, metadata) in paths { - let mut current = &mut root; - let components: Vec = path - .components() - .filter_map(|comp| match comp { - std::path::Component::Normal(s) => s.to_str().map(|s| s.to_string()), - _ => None, - }) - .collect(); - - for (i, comp) in components.iter().enumerate() { - let is_leaf = i == components.len() - 1; - let child = current - .children - .entry(comp.clone()) - .or_insert_with(|| TreeNode::new(comp.clone())); - - // If this is the leaf node, store the metadata - if is_leaf { - child.metadata = Some((metadata.id.clone(), metadata.version.clone())); - } - - current = child; - } - } - - // Convert tree to string representation - let mut result = String::new(); - let is_root = true; - let prefix = String::new(); - let last_stack = vec![true]; // Root is always "last" - - add_tree_node_to_string(&root, &mut result, is_root, &prefix, &last_stack); - - result -} - -/// Internal tree node structure for building the path tree -#[derive(Debug)] -struct TreeNode { - name: String, - children: BTreeMap, // Use BTreeMap for sorted output - metadata: Option<(String, String)>, // Store (id, version) for leaf nodes -} - -impl TreeNode { - fn new(name: String) -> Self { - Self { - name, - children: BTreeMap::new(), - metadata: None, - } - } -} - -/// Recursively add tree node to string representation -fn add_tree_node_to_string( - node: &TreeNode, - result: &mut String, - is_root: bool, - prefix: &str, - last_stack: &[bool], -) { - if !is_root { - // Add the tree prefix for this node - for &is_last in &last_stack[1..] { - if is_last { - result.push_str(" "); - } else { - result.push_str("│ "); - } - } - - // Add the connector for this node - if let Some(&is_last) = last_stack.last() { - if is_last { - result.push_str("└── "); - } else { - result.push_str("├── "); - } - } - - // Add node name - result.push_str(&node.name); - - // Add metadata for leaf nodes - if let Some((id, version)) = &node.metadata { - // Truncate id to first 11 characters - let truncated_id = if id.len() > 11 { &id[..11] } else { id }; - result.push_str(&format!(" [{}|{}]", truncated_id, version)); - } - - result.push('\n'); - } - - // Process children - let child_count = node.children.len(); - for (i, (_, child)) in node.children.iter().enumerate() { - let is_last_child = i == child_count - 1; - let mut new_last_stack = last_stack.to_vec(); - new_last_stack.push(is_last_child); - - add_tree_node_to_string(child, result, false, prefix, &new_last_stack); - } -} +pub mod colorful; +pub mod table; diff --git a/utils/src/display/colorful.rs b/utils/src/display/colorful.rs new file mode 100644 index 0000000..7daa6f2 --- /dev/null +++ b/utils/src/display/colorful.rs @@ -0,0 +1,275 @@ +use std::collections::VecDeque; + +use crossterm::style::Stylize; + +/// Trait for adding markdown formatting to strings +pub trait Colorful { + fn colorful(&self) -> String; +} + +impl Colorful for &str { + fn colorful(&self) -> String { + colorful(self) + } +} + +impl Colorful for String { + fn colorful(&self) -> String { + colorful(self) + } +} + +/// Converts a string to colored/formatted text with ANSI escape codes. +/// +/// Supported syntax: +/// - Bold: `**text**` +/// - Italic: `*text*` +/// - Underline: `_text_` +/// - Angle-bracketed content: `` (displayed as cyan) +/// - Inline code: `` `text` `` (displayed as green) +/// - Color tags: `[[color_name]]` and `[[/]]` to close color +/// - Escape characters: `\*`, `\<`, `\>`, `` \` ``, `\_` for literal characters +/// +/// Color tags support the following color names: +/// Color tags support the following color names: +/// +/// | Type | Color Names | +/// |-----------------------|-----------------------------------------------------------------------------| +/// | Standard colors | `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white` | +/// | Bright colors | `bright_black` | +/// | | `bright_red` | +/// | | `bright_green` | +/// | | `bright_yellow` | +/// | | `bright_blue` | +/// | | `bright_magenta` | +/// | | `bright_cyan` | +/// | | `bright_white` | +/// | Bright color shorthands | `b_black` | +/// | | `b_red` | +/// | | `b_green` | +/// | | `b_yellow` | +/// | | `b_blue` | +/// | | `b_magenta` | +/// | | `b_cyan` | +/// | | `b_white` | +/// | Gray colors | `gray`/`grey` | +/// | | `bright_gray`/`bright_grey` | +/// | | `b_gray`/`b_grey` | +/// +/// Color tags can be nested, `[[/]]` will close the most recently opened color tag. +/// +/// # Arguments +/// * `text` - The text to format, can be any type that implements `AsRef` +/// +/// # Returns +/// Returns a `String` containing ANSI escape codes that can display colored/formatted text in ANSI-supported terminals. +/// +/// # Examples +/// ``` +/// use testing::fmt::colorful; +/// +/// let formatted = colorful("Hello **world**!"); +/// println!("{}", formatted); +/// +/// let colored = colorful("[[red]]Red text[[/]] and normal text"); +/// println!("{}", colored); +/// +/// let nested = colorful("[[blue]]Blue [[green]]Green[[/]] Blue[[/]] normal"); +/// println!("{}", nested); +/// ``` +pub fn colorful(text: impl AsRef) -> String { + let text = text.as_ref().trim(); + let mut result = String::new(); + let mut color_stack: VecDeque = VecDeque::new(); + + let chars: Vec = text.chars().collect(); + let mut i = 0; + + while i < chars.len() { + // Check for escape character \ + if chars[i] == '\\' && i + 1 < chars.len() { + let escaped_char = chars[i + 1]; + // Only escape specific characters + if matches!(escaped_char, '*' | '<' | '>' | '`' | '_') { + let mut escaped_text = escaped_char.to_string(); + apply_color_stack(&mut escaped_text, &color_stack); + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' { + if let Some(end) = find_tag_end(&chars, i) { + let tag_content: String = chars[i + 2..end].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + } else { + // It's a color tag + color_stack.push_back(tag_content.clone()); + } + i = end + 2; + continue; + } + } + + // Check for bold **text** + if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { + if let Some(end) = find_matching(&chars, i + 2, "**") { + let bold_text: String = chars[i + 2..end].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 2; + continue; + } + } + + // Check for italic *text* + if chars[i] == '*' { + if let Some(end) = find_matching(&chars, i + 1, "*") { + let italic_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for underline _text_ + if chars[i] == '_' { + if let Some(end) = find_matching(&chars, i + 1, "_") { + let underline_text: String = chars[i + 1..end].iter().collect(); + let mut formatted_text = format!("\x1b[4m{}\x1b[0m", underline_text); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for angle-bracketed content + if chars[i] == '<' { + if let Some(end) = find_matching(&chars, i + 1, ">") { + // Include the angle brackets in the output + let angle_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Check for inline code `text` + if chars[i] == '`' { + if let Some(end) = find_matching(&chars, i + 1, "`") { + // Include the backticks in the output + let code_text: String = chars[i..=end].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + apply_color_stack(&mut formatted_text, &color_stack); + result.push_str(&formatted_text); + i = end + 1; + continue; + } + } + + // Regular character + let mut current_char = chars[i].to_string(); + apply_color_stack(&mut current_char, &color_stack); + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to find matching delimiter +fn find_matching(chars: &[char], start: usize, delimiter: &str) -> Option { + let delim_chars: Vec = delimiter.chars().collect(); + let delim_len = delim_chars.len(); + + let mut j = start; + while j < chars.len() { + if delim_len == 1 { + if chars[j] == delim_chars[0] { + return Some(j); + } + } else if j + 1 < chars.len() + && chars[j] == delim_chars[0] + && chars[j + 1] == delim_chars[1] + { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to find color tag end +fn find_tag_end(chars: &[char], start: usize) -> Option { + let mut j = start + 2; + while j + 1 < chars.len() { + if chars[j] == ']' && chars[j + 1] == ']' { + return Some(j); + } + j += 1; + } + None +} + +// Helper function to apply color stack to text +fn apply_color_stack(text: &mut String, color_stack: &VecDeque) { + let mut result = text.clone(); + for color in color_stack.iter().rev() { + result = apply_color(&result, color); + } + *text = result; +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef, color_name: impl AsRef) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.dark_grey().to_string(), + "red" => text.dark_red().to_string(), + "green" => text.dark_green().to_string(), + "yellow" => text.dark_yellow().to_string(), + "blue" => text.dark_blue().to_string(), + "magenta" => text.dark_magenta().to_string(), + "cyan" => text.dark_cyan().to_string(), + "white" => text.white().to_string(), + "bright_black" => text.black().to_string(), + "bright_red" => text.red().to_string(), + "bright_green" => text.green().to_string(), + "bright_yellow" => text.yellow().to_string(), + "bright_blue" => text.blue().to_string(), + "bright_magenta" => text.magenta().to_string(), + "bright_cyan" => text.cyan().to_string(), + "bright_white" => text.white().to_string(), + + // Short aliases for bright colors + "b_black" => text.black().to_string(), + "b_red" => text.red().to_string(), + "b_green" => text.green().to_string(), + "b_yellow" => text.yellow().to_string(), + "b_blue" => text.blue().to_string(), + "b_magenta" => text.magenta().to_string(), + "b_cyan" => text.cyan().to_string(), + "b_white" => text.white().to_string(), + + // Gray colors using truecolor + "gray" | "grey" => text.grey().to_string(), + "bright_gray" | "bright_grey" => text.white().to_string(), + "b_gray" | "b_grey" => text.white().to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} diff --git a/utils/src/display/table.rs b/utils/src/display/table.rs new file mode 100644 index 0000000..ae745d8 --- /dev/null +++ b/utils/src/display/table.rs @@ -0,0 +1,143 @@ +pub struct Table { + items: Vec, + line: Vec>, + length: Vec, + padding: usize, +} + +impl Table { + /// Create a new Table + pub fn new(items: Vec>) -> Self { + Self::new_with_padding(items, 2) + } + + /// Create a new Table with padding + pub fn new_with_padding(items: Vec>, padding: usize) -> Self { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + let mut length = Vec::with_capacity(items.len()); + + for item in &items { + length.push(display_width(item)); + } + + Table { + items, + padding, + line: Vec::new(), + length, + } + } + + /// Push a new row of items to the table + pub fn push_item(&mut self, items: Vec>) { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + + let mut processed_items = Vec::with_capacity(self.items.len()); + + for i in 0..self.items.len() { + if i < items.len() { + processed_items.push(items[i].clone()); + } else { + processed_items.push(String::new()); + } + } + + for (i, d) in processed_items.iter().enumerate() { + let d_len = display_width(d); + if d_len > self.length[i] { + self.length[i] = d_len; + } + } + + self.line.push(processed_items); + } + + /// Insert a new row of items at the specified index + pub fn insert_item(&mut self, index: usize, items: Vec>) { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + + let mut processed_items = Vec::with_capacity(self.items.len()); + + for i in 0..self.items.len() { + if i < items.len() { + processed_items.push(items[i].clone()); + } else { + processed_items.push(String::new()); + } + } + + for (i, d) in processed_items.iter().enumerate() { + let d_len = display_width(d); + if d_len > self.length[i] { + self.length[i] = d_len; + } + } + + self.line.insert(index, processed_items); + } + + /// Get the current maximum column widths + fn get_column_widths(&self) -> &[usize] { + &self.length + } +} + +impl std::fmt::Display for Table { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let column_widths = self.get_column_widths(); + + // Build the header row + let header: Vec = self + .items + .iter() + .enumerate() + .map(|(i, item)| { + let target_width = column_widths[i] + self.padding; + let current_width = display_width(item); + let space_count = target_width - current_width; + let space = " ".repeat(space_count); + let result = format!("{}{}", item, space); + result + }) + .collect(); + writeln!(f, "{}", header.join(""))?; + + // Build each data row + for row in &self.line { + let formatted_row: Vec = row + .iter() + .enumerate() + .map(|(i, cell)| { + let target_width = column_widths[i] + self.padding; + let current_width = display_width(cell); + let space_count = target_width - current_width; + let spaces = " ".repeat(space_count); + let result = format!("{}{}", cell, spaces); + result + }) + .collect(); + writeln!(f, "{}", formatted_row.join(""))?; + } + + Ok(()) + } +} + +pub fn display_width(s: &str) -> usize { + // Filter out ANSI escape sequences before calculating width + let filtered_bytes = strip_ansi_escapes::strip(s); + let filtered_str = match std::str::from_utf8(&filtered_bytes) { + Ok(s) => s, + Err(_) => s, // Fallback to original string if UTF-8 conversion fails + }; + + let mut width = 0; + for c in filtered_str.chars() { + if c.is_ascii() { + width += 1; + } else { + width += 2; + } + } + width +} diff --git a/utils/src/env.rs b/utils/src/env.rs deleted file mode 100644 index 1834cd3..0000000 --- a/utils/src/env.rs +++ /dev/null @@ -1,107 +0,0 @@ -use std::path::PathBuf; - -/// Returns the current locale string based on environment variables. -/// -/// The function checks for locale settings in the following order: -/// 1. JV_LANG environment variable -/// 2. APP_LANG environment variable -/// 3. LANG environment variable (extracts base language before dot and replaces underscores with hyphens) -/// 4. Defaults to "en" if no locale environment variables are found -/// -/// # Returns -/// A String containing the detected locale code -pub fn current_locales() -> String { - if let Ok(lang) = std::env::var("JV_LANG") { - return lang; - } - - if let Ok(lang) = std::env::var("APP_LANG") { - return lang; - } - - if let Ok(lang) = std::env::var("LANG") { - if let Some(base_lang) = lang.split('.').next() { - return base_lang.replace('_', "-"); - } - return lang; - } - - "en".to_string() -} - -/// Checks if auto update is enabled based on environment variables. -/// -/// The function checks the JV_AUTO_UPDATE environment variable and compares -/// its value (after trimming and converting to lowercase) against known -/// positive and negative values. -/// -/// # Returns -/// `true` if the value matches "yes", "y", or "true" -/// `false` if the value matches "no", "n", or "false", or if the variable is not set -pub fn enable_auto_update() -> bool { - if let Ok(auto_update) = std::env::var("JV_AUTO_UPDATE") { - let normalized = auto_update.trim().to_lowercase(); - match normalized.as_str() { - "yes" | "y" | "true" => return true, - "no" | "n" | "false" => return false, - _ => {} - } - } - false -} - -/// Gets the auto update expiration time based on environment variables. -/// -/// The function checks the JV_OUTDATED_MINUTES environment variable. -/// Requires JV_AUTO_UPDATE to be enabled. -/// Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed. -/// -/// # Returns -/// - When the set number is < 0, timeout-based update is disabled -/// - When the set number = 0, update runs every time (not recommended) -/// - When the set number > 0, update according to the specified time -/// - If not set or conversion error occurs, the default is -1 -pub fn auto_update_outdate() -> i64 { - if !enable_auto_update() { - return -1; - } - - match std::env::var("JV_OUTDATED_MINUTES") { - Ok(value) => match value.trim().parse::() { - Ok(num) => num, - Err(_) => -1, - }, - Err(_) => -1, - } -} - -/// Gets the default text editor based on environment variables. -/// -/// The function checks the JV_TEXT_EDITOR and EDITOR environment variables -/// and returns their values if they are set. If neither variable is set, -/// it returns "jvii" as the default editor. -/// -/// # Returns -/// A String containing the default text editor -pub async fn get_default_editor() -> String { - if let Ok(editor) = std::env::var("JV_TEXT_EDITOR") { - return editor; - } - - if let Ok(editor) = std::env::var("EDITOR") { - return editor; - } - - "jvii".to_string() -} - -/// Get temporary file path -pub fn current_tempfile_path(name: &str) -> Option { - dirs::config_local_dir().map(|path| { - if cfg!(target_os = "linux") { - path.join("jvcs").join(".temp").join(name) - } else { - path.join("JustEnoughVCS").join(".temp").join(name) - } - }) -} diff --git a/utils/src/fs.rs b/utils/src/fs.rs deleted file mode 100644 index 0050cf1..0000000 --- a/utils/src/fs.rs +++ /dev/null @@ -1,40 +0,0 @@ -pub async fn move_across_partitions( - source_path: impl AsRef, - dest_path: impl AsRef, -) -> Result<(), std::io::Error> { - let source_path = source_path.as_ref(); - let dest_path = dest_path.as_ref(); - if !source_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Source file does not exist", - )); - } - - if let Ok(()) = std::fs::rename(source_path, dest_path) { - return Ok(()); - } - - std::fs::copy(source_path, dest_path)?; - std::fs::remove_file(source_path)?; - - Ok(()) -} - -pub async fn copy_across_partitions( - source_path: impl AsRef, - dest_path: impl AsRef, -) -> Result<(), std::io::Error> { - let source_path = source_path.as_ref(); - let dest_path = dest_path.as_ref(); - if !source_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "Source file does not exist", - )); - } - - std::fs::copy(source_path, dest_path)?; - - Ok(()) -} diff --git a/utils/src/globber.rs b/utils/src/globber.rs deleted file mode 100644 index 7021898..0000000 --- a/utils/src/globber.rs +++ /dev/null @@ -1,279 +0,0 @@ -use std::{io::Error, path::PathBuf, str::FromStr}; - -use just_fmt::fmt_path::fmt_path_str; - -use crate::globber::constants::{SPLIT_STR, get_base_dir_current}; - -pub struct Globber { - pattern: String, - base: PathBuf, - names: Vec, -} - -#[allow(dead_code)] -impl Globber { - pub fn new(pattern: String, base: PathBuf) -> Self { - Self { - pattern, - base, - names: Vec::new(), - } - } - - pub fn names(&self) -> Vec<&String> { - self.names.iter().collect() - } - - pub fn base(&self) -> &PathBuf { - &self.base - } - - pub fn into_names(self) -> Vec { - self.names - } - - pub fn paths(&self) -> Vec { - self.names.iter().map(|n| self.base.join(n)).collect() - } - - pub fn glob(mut self, get_names: F) -> Result - where - F: Fn(PathBuf) -> Vec, - { - let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern); - - let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) { - let (path_part, pattern_part) = full_path.split_at(last_split); - let mut path = path_part.to_string(); - if !path.ends_with(SPLIT_STR) { - path.push_str(SPLIT_STR); - } - let Ok(result) = fmt_path_str(&path) else { - return Err(Error::new( - std::io::ErrorKind::InvalidInput, - format!("Invalid path: \"{}\"", &path), - )); - }; - (result, pattern_part[SPLIT_STR.len()..].to_string()) - } else { - (String::default(), full_path) - }; - - self.base = match PathBuf::from_str(&path) { - Ok(r) => r, - Err(_) => { - return Err(Error::new( - std::io::ErrorKind::InvalidInput, - format!("Invalid path: \"{}\"", &path), - )); - } - }; - - let pattern = if pattern.is_empty() { - "*".to_string() - } else if pattern == "." { - "*".to_string() - } else if pattern.ends_with(SPLIT_STR) { - format!("{}*", pattern) - } else { - pattern - }; - - if !pattern.contains('*') && !pattern.contains('?') { - self.names = vec![pattern]; - return Ok(self); - } - - let mut collected = Vec::new(); - - collect_files(&path.into(), "./".to_string(), &mut collected, &get_names); - fn collect_files( - base: &PathBuf, - current: String, - file_names: &mut Vec, - get_names: &F, - ) where - F: Fn(PathBuf) -> Vec, - { - let current_path = if current.is_empty() { - base.clone() - } else { - base.join(¤t) - }; - - let items = get_names(current_path); - for item in items { - match item { - GlobItem::File(file_name) => { - let relative_path = { - fmt_path_str(format!("{}{}{}", current, SPLIT_STR, file_name)) - .unwrap_or_default() - }; - file_names.push(relative_path) - } - GlobItem::Directory(dir_name) => { - let new_current = { - fmt_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name)) - .unwrap_or_default() - }; - collect_files(base, new_current, file_names, get_names); - } - } - } - } - - self.names = collected - .iter() - .filter_map(|name| match_pattern(name, &pattern)) - .collect(); - - Ok(self) - } -} - -fn match_pattern(name: &str, pattern: &str) -> Option { - if pattern.is_empty() { - return None; - } - - let name_chars: Vec = name.chars().collect(); - let pattern_chars: Vec = pattern.chars().collect(); - - let mut name_idx = 0; - let mut pattern_idx = 0; - let mut star_idx = -1; - let mut match_idx = -1; - - while name_idx < name_chars.len() { - if pattern_idx < pattern_chars.len() - && (pattern_chars[pattern_idx] == '?' - || pattern_chars[pattern_idx] == name_chars[name_idx]) - { - name_idx += 1; - pattern_idx += 1; - } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { - star_idx = pattern_idx as i32; - match_idx = name_idx as i32; - pattern_idx += 1; - } else if star_idx != -1 { - pattern_idx = (star_idx + 1) as usize; - match_idx += 1; - name_idx = match_idx as usize; - } else { - return None; - } - } - - while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { - pattern_idx += 1; - } - - if pattern_idx == pattern_chars.len() { - Some(name.to_string()) - } else { - None - } -} - -impl> From for Globber { - fn from(pattern: T) -> Self { - let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string()); - Self::new(pattern, base_dir) - } -} - -#[derive(Debug, Clone, Hash)] -pub enum GlobItem { - File(String), - Directory(String), -} - -impl PartialEq for GlobItem { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (GlobItem::File(a), GlobItem::File(b)) => a == b, - (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b, - _ => false, - } - } -} - -impl std::fmt::Display for GlobItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - GlobItem::File(name) => write!(f, "{}", name), - GlobItem::Directory(name) => write!(f, "{}", name), - } - } -} - -impl Eq for GlobItem {} - -pub mod constants { - use std::{env::current_dir, path::PathBuf}; - - #[cfg(unix)] - pub(crate) const CURRENT_DIR_PREFIX: &str = "./"; - #[cfg(windows)] - pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\"; - - #[cfg(unix)] - pub(crate) const USER_DIR_PREFIX: &str = "~"; - #[cfg(windows)] - pub(crate) const USER_DIR_PREFIX: &str = "~\\"; - - #[cfg(unix)] - pub(crate) const ROOT_DIR_PREFIX: &str = "/"; - #[cfg(windows)] - pub(crate) const ROOT_DIR_PREFIX: &str = "\\"; - - #[cfg(unix)] - pub(crate) const SPLIT_STR: &str = "/"; - #[cfg(windows)] - pub(crate) const SPLIT_STR: &str = "\\"; - - pub fn get_base_dir_current(input: String) -> (PathBuf, String) { - get_base_dir(input, current_dir().unwrap_or_default()) - } - - pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) { - if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) { - (current_dir, remaining.to_string()) - } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) { - (dirs::home_dir().unwrap_or_default(), remaining.to_string()) - } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) { - { - #[cfg(unix)] - { - (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string()) - } - #[cfg(windows)] - { - let current_drive = current_dir - .components() - .find_map(|comp| { - if let std::path::Component::Prefix(prefix_component) = comp { - Some(prefix_component) - } else { - None - } - }) - .and_then(|prefix_component| match prefix_component.kind() { - std::path::Prefix::Disk(drive_letter) - | std::path::Prefix::VerbatimDisk(drive_letter) => { - Some((drive_letter as char).to_string()) - } - _ => None, - }) - .unwrap_or_else(|| "C".to_string()); - ( - PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)), - remaining.to_string(), - ) - } - } - } else { - (current_dir, input) - } - } -} diff --git a/utils/src/input.rs b/utils/src/input.rs deleted file mode 100644 index bc67d90..0000000 --- a/utils/src/input.rs +++ /dev/null @@ -1,151 +0,0 @@ -use tokio::{fs, process::Command}; - -use crate::env::get_default_editor; - -/// Confirm the current operation -/// Waits for user input of 'y' or 'n' -pub async fn confirm_hint(text: impl Into) -> bool { - use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; - - let prompt = text.into().trim().to_string(); - - let mut stdout = io::stdout(); - let mut stdin = BufReader::new(io::stdin()); - - stdout - .write_all(prompt.as_bytes()) - .await - .expect("Failed to write prompt"); - stdout.flush().await.expect("Failed to flush stdout"); - - let mut input = String::new(); - stdin - .read_line(&mut input) - .await - .expect("Failed to read input"); - - input.trim().eq_ignore_ascii_case("y") -} - -/// Confirm the current operation, or execute a closure if rejected -/// Waits for user input of 'y' or 'n' -/// If 'n' is entered, executes the provided closure and returns false -pub async fn confirm_hint_or(text: impl Into, on_reject: F) -> bool -where - F: FnOnce(), -{ - let confirmed = confirm_hint(text).await; - if !confirmed { - on_reject(); - } - confirmed -} - -/// Confirm the current operation, and execute a closure if confirmed -/// Waits for user input of 'y' or 'n' -/// If 'y' is entered, executes the provided closure and returns true -pub async fn confirm_hint_then(text: impl Into, on_confirm: F) -> bool -where - F: FnOnce(), -{ - let confirmed = confirm_hint(text).await; - if confirmed { - on_confirm(); - } - confirmed -} - -/// Input text using the system editor -/// Opens the system editor (from EDITOR environment variable) with default text in a cache file, -/// then reads back the modified content after the editor closes, removing comment lines -pub async fn input_with_editor( - default_text: impl AsRef, - cache_file: impl AsRef, - comment_char: impl AsRef, -) -> Result { - input_with_editor_cutsom( - default_text, - cache_file, - comment_char, - get_default_editor().await, - ) - .await -} - -pub async fn input_with_editor_cutsom( - default_text: impl AsRef, - cache_file: impl AsRef, - comment_char: impl AsRef, - editor: String, -) -> Result { - let cache_path = cache_file.as_ref(); - let default_content = default_text.as_ref(); - let comment_prefix = comment_char.as_ref(); - - // Write default text to cache file - fs::write(cache_path, default_content).await?; - - // Open editor with cache file - let status = Command::new(editor).arg(cache_path).status().await?; - - if !status.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Editor exited with non-zero status", - )); - } - - // Read the modified content - let content = fs::read_to_string(cache_path).await?; - - // Remove comment lines and trim - let processed_content: String = content - .lines() - .filter_map(|line| { - let trimmed = line.trim(); - if trimmed.starts_with(comment_prefix) { - None - } else { - Some(line) - } - }) - .collect::>() - .join("\n"); - - // Delete the cache file - let _ = fs::remove_file(cache_path).await; - - Ok(processed_content) -} - -/// Show text using the system pager (less) -/// Opens the system pager (less) with the given text content written to the specified file -/// If less is not found, directly outputs the content to stdout -pub async fn show_in_pager( - content: impl AsRef, - cache_file: impl AsRef, -) -> Result<(), std::io::Error> { - let content_str = content.as_ref(); - let cache_path = cache_file.as_ref(); - - // Write content to cache file - fs::write(cache_path, content_str).await?; - - // Try to use less first - let status = Command::new("less").arg(cache_path).status().await; - - match status { - Ok(status) if status.success() => Ok(()), - _ => { - // If less failed, output directly to stdout - use tokio::io::{self, AsyncWriteExt}; - let mut stdout = io::stdout(); - stdout - .write_all(content_str.as_bytes()) - .await - .expect("Failed to write content"); - stdout.flush().await.expect("Failed to flush stdout"); - Ok(()) - } - } -} diff --git a/utils/src/lazy_macros.rs b/utils/src/lazy_macros.rs deleted file mode 100644 index f1cb75e..0000000 --- a/utils/src/lazy_macros.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// A macro for creating a `Vec` from string literals. -/// -/// # Examples -/// ``` -/// # use cli_utils::string_vec; -/// let v = string_vec!["hello", "world"]; -/// assert_eq!(v, vec!["hello".to_string(), "world".to_string()]); -/// ``` -#[macro_export] -macro_rules! string_vec { - ($($elem:expr),* $(,)?) => { - vec![$($elem.to_string()),*] - }; -} diff --git a/utils/src/legacy.rs b/utils/src/legacy.rs new file mode 100644 index 0000000..682c679 --- /dev/null +++ b/utils/src/legacy.rs @@ -0,0 +1,9 @@ +pub mod display; +pub mod env; +pub mod fs; +pub mod globber; +pub mod input; +pub mod levenshtein_distance; +pub mod logger; +pub mod push_version; +pub mod socket_addr_helper; diff --git a/utils/src/legacy/display.rs b/utils/src/legacy/display.rs new file mode 100644 index 0000000..fc94d90 --- /dev/null +++ b/utils/src/legacy/display.rs @@ -0,0 +1,488 @@ +use colored::*; +use just_enough_vcs::lib::data::sheet::SheetMappingMetadata; +use std::{ + collections::{BTreeMap, HashMap, VecDeque}, + path::PathBuf, +}; + +pub struct SimpleTable { + items: Vec, + line: Vec>, + length: Vec, + padding: usize, +} + +impl SimpleTable { + /// Create a new Table + pub fn new(items: Vec>) -> Self { + Self::new_with_padding(items, 2) + } + + /// Create a new Table with padding + pub fn new_with_padding(items: Vec>, padding: usize) -> Self { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + let mut length = Vec::with_capacity(items.len()); + + for item in &items { + length.push(display_width(item)); + } + + SimpleTable { + items, + padding, + line: Vec::new(), + length, + } + } + + /// Push a new row of items to the table + pub fn push_item(&mut self, items: Vec>) { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + + let mut processed_items = Vec::with_capacity(self.items.len()); + + for i in 0..self.items.len() { + if i < items.len() { + processed_items.push(items[i].clone()); + } else { + processed_items.push(String::new()); + } + } + + for (i, d) in processed_items.iter().enumerate() { + let d_len = display_width(d); + if d_len > self.length[i] { + self.length[i] = d_len; + } + } + + self.line.push(processed_items); + } + + /// Insert a new row of items at the specified index + pub fn insert_item(&mut self, index: usize, items: Vec>) { + let items: Vec = items.into_iter().map(|v| v.into()).collect(); + + let mut processed_items = Vec::with_capacity(self.items.len()); + + for i in 0..self.items.len() { + if i < items.len() { + processed_items.push(items[i].clone()); + } else { + processed_items.push(String::new()); + } + } + + for (i, d) in processed_items.iter().enumerate() { + let d_len = display_width(d); + if d_len > self.length[i] { + self.length[i] = d_len; + } + } + + self.line.insert(index, processed_items); + } + + /// Get the current maximum column widths + fn get_column_widths(&self) -> &[usize] { + &self.length + } +} + +impl std::fmt::Display for SimpleTable { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let column_widths = self.get_column_widths(); + + // Build the header row + let header: Vec = self + .items + .iter() + .enumerate() + .map(|(i, item)| { + let target_width = column_widths[i] + self.padding; + let current_width = display_width(item); + let space_count = target_width - current_width; + let space = " ".repeat(space_count); + let result = format!("{}{}", item, space); + result + }) + .collect(); + writeln!(f, "{}", header.join(""))?; + + // Build each data row + for row in &self.line { + let formatted_row: Vec = row + .iter() + .enumerate() + .map(|(i, cell)| { + let target_width = column_widths[i] + self.padding; + let current_width = display_width(cell); + let space_count = target_width - current_width; + let spaces = " ".repeat(space_count); + let result = format!("{}{}", cell, spaces); + result + }) + .collect(); + writeln!(f, "{}", formatted_row.join(""))?; + } + + Ok(()) + } +} + +pub fn display_width(s: &str) -> usize { + // Filter out ANSI escape sequences before calculating width + let filtered_bytes = strip_ansi_escapes::strip(s); + let filtered_str = match std::str::from_utf8(&filtered_bytes) { + Ok(s) => s, + Err(_) => s, // Fallback to original string if UTF-8 conversion fails + }; + + let mut width = 0; + for c in filtered_str.chars() { + if c.is_ascii() { + width += 1; + } else { + width += 2; + } + } + width +} + +/// Convert byte size to a human-readable string format +/// +/// Automatically selects the appropriate unit (B, KB, MB, GB, TB) based on the byte size +/// and formats it as a string with two decimal places +pub fn size_str(total_size: usize) -> String { + if total_size < 1024 { + format!("{} B", total_size) + } else if total_size < 1024 * 1024 { + format!("{:.2} KB", total_size as f64 / 1024.0) + } else if total_size < 1024 * 1024 * 1024 { + format!("{:.2} MB", total_size as f64 / (1024.0 * 1024.0)) + } else if total_size < 1024 * 1024 * 1024 * 1024 { + format!("{:.2} GB", total_size as f64 / (1024.0 * 1024.0 * 1024.0)) + } else { + format!( + "{:.2} TB", + total_size as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0) + ) + } +} + +// Convert the Markdown formatted text into a format supported by the command line +pub fn md(text: impl AsRef) -> String { + let text = text.as_ref().trim(); + let mut result = String::new(); + let mut color_stack: VecDeque = VecDeque::new(); + + let mut i = 0; + let chars: Vec = text.chars().collect(); + + while i < chars.len() { + // Check for escape character \ + if chars[i] == '\\' && i + 1 < chars.len() { + let escaped_char = chars[i + 1]; + // Only escape specific characters + if matches!(escaped_char, '*' | '<' | '>' | '`') { + let mut escaped_text = escaped_char.to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + escaped_text = apply_color(&escaped_text, color); + } + + result.push_str(&escaped_text); + i += 2; + continue; + } + } + + // Check for color tag start [[color]] + if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '[' { + let mut j = i + 2; + while j < chars.len() + && !(chars[j] == ']' && j + 1 < chars.len() && chars[j + 1] == ']') + { + j += 1; + } + + if j + 1 < chars.len() { + let tag_content: String = chars[i + 2..j].iter().collect(); + + // Check if it's a closing tag [[/]] + if tag_content == "/" { + color_stack.pop_back(); + i = j + 2; + continue; + } + + // It's a color tag + color_stack.push_back(tag_content.clone()); + i = j + 2; + continue; + } + } + + // Check for bold **text** + if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' { + let mut j = i + 2; + while j + 1 < chars.len() && !(chars[j] == '*' && chars[j + 1] == '*') { + j += 1; + } + + if j + 1 < chars.len() { + let bold_text: String = chars[i + 2..j].iter().collect(); + let mut formatted_text = bold_text.bold().to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + formatted_text = apply_color(&formatted_text, color); + } + + result.push_str(&formatted_text); + i = j + 2; + continue; + } + } + + // Check for italic *text* + if chars[i] == '*' { + let mut j = i + 1; + while j < chars.len() && chars[j] != '*' { + j += 1; + } + + if j < chars.len() { + let italic_text: String = chars[i + 1..j].iter().collect(); + let mut formatted_text = italic_text.italic().to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + formatted_text = apply_color(&formatted_text, color); + } + + result.push_str(&formatted_text); + i = j + 1; + continue; + } + } + + // Check for angle-bracketed content + if chars[i] == '<' { + let mut j = i + 1; + while j < chars.len() && chars[j] != '>' { + j += 1; + } + + if j < chars.len() { + // Include the angle brackets in the output + let angle_text: String = chars[i..=j].iter().collect(); + let mut formatted_text = angle_text.cyan().to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + formatted_text = apply_color(&formatted_text, color); + } + + result.push_str(&formatted_text); + i = j + 1; + continue; + } + } + + // Check for inline code `text` + if chars[i] == '`' { + let mut j = i + 1; + while j < chars.len() && chars[j] != '`' { + j += 1; + } + + if j < chars.len() { + // Include the backticks in the output + let code_text: String = chars[i..=j].iter().collect(); + let mut formatted_text = code_text.green().to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + formatted_text = apply_color(&formatted_text, color); + } + + result.push_str(&formatted_text); + i = j + 1; + continue; + } + } + + // Regular character + let mut current_char = chars[i].to_string(); + + // Apply current color stack + for color in color_stack.iter().rev() { + current_char = apply_color(¤t_char, color); + } + + result.push_str(¤t_char); + i += 1; + } + + result +} + +// Helper function to apply color to text +fn apply_color(text: impl AsRef, color_name: impl AsRef) -> String { + let text = text.as_ref(); + let color_name = color_name.as_ref(); + match color_name { + // Normal colors + "black" => text.black().to_string(), + "red" => text.red().to_string(), + "green" => text.green().to_string(), + "yellow" => text.yellow().to_string(), + "blue" => text.blue().to_string(), + "magenta" => text.magenta().to_string(), + "cyan" => text.cyan().to_string(), + "white" => text.white().to_string(), + "bright_black" => text.bright_black().to_string(), + "bright_red" => text.bright_red().to_string(), + "bright_green" => text.bright_green().to_string(), + "bright_yellow" => text.bright_yellow().to_string(), + "bright_blue" => text.bright_blue().to_string(), + "bright_magenta" => text.bright_magenta().to_string(), + "bright_cyan" => text.bright_cyan().to_string(), + "bright_white" => text.bright_white().to_string(), + + // Short aliases for bright colors + "b_black" => text.bright_black().to_string(), + "b_red" => text.bright_red().to_string(), + "b_green" => text.bright_green().to_string(), + "b_yellow" => text.bright_yellow().to_string(), + "b_blue" => text.bright_blue().to_string(), + "b_magenta" => text.bright_magenta().to_string(), + "b_cyan" => text.bright_cyan().to_string(), + "b_white" => text.bright_white().to_string(), + + // Gray colors using truecolor + "gray" | "grey" => text.truecolor(128, 128, 128).to_string(), + "bright_gray" | "bright_grey" => text.truecolor(192, 192, 192).to_string(), + "b_gray" | "b_grey" => text.truecolor(192, 192, 192).to_string(), + + // Default to white if color not recognized + _ => text.to_string(), + } +} + +/// Render a HashMap of PathBuf to SheetMappingMetadata as a tree string. +pub fn render_share_path_tree(paths: &HashMap) -> String { + if paths.is_empty() { + return String::new(); + } + + // Collect all path components into a tree structure + let mut root = TreeNode::new("".to_string()); + + for (path, metadata) in paths { + let mut current = &mut root; + let components: Vec = path + .components() + .filter_map(|comp| match comp { + std::path::Component::Normal(s) => s.to_str().map(|s| s.to_string()), + _ => None, + }) + .collect(); + + for (i, comp) in components.iter().enumerate() { + let is_leaf = i == components.len() - 1; + let child = current + .children + .entry(comp.clone()) + .or_insert_with(|| TreeNode::new(comp.clone())); + + // If this is the leaf node, store the metadata + if is_leaf { + child.metadata = Some((metadata.id.clone(), metadata.version.clone())); + } + + current = child; + } + } + + // Convert tree to string representation + let mut result = String::new(); + let is_root = true; + let prefix = String::new(); + let last_stack = vec![true]; // Root is always "last" + + add_tree_node_to_string(&root, &mut result, is_root, &prefix, &last_stack); + + result +} + +/// Internal tree node structure for building the path tree +#[derive(Debug)] +struct TreeNode { + name: String, + children: BTreeMap, // Use BTreeMap for sorted output + metadata: Option<(String, String)>, // Store (id, version) for leaf nodes +} + +impl TreeNode { + fn new(name: String) -> Self { + Self { + name, + children: BTreeMap::new(), + metadata: None, + } + } +} + +/// Recursively add tree node to string representation +fn add_tree_node_to_string( + node: &TreeNode, + result: &mut String, + is_root: bool, + prefix: &str, + last_stack: &[bool], +) { + if !is_root { + // Add the tree prefix for this node + for &is_last in &last_stack[1..] { + if is_last { + result.push_str(" "); + } else { + result.push_str("│ "); + } + } + + // Add the connector for this node + if let Some(&is_last) = last_stack.last() { + if is_last { + result.push_str("└── "); + } else { + result.push_str("├── "); + } + } + + // Add node name + result.push_str(&node.name); + + // Add metadata for leaf nodes + if let Some((id, version)) = &node.metadata { + // Truncate id to first 11 characters + let truncated_id = if id.len() > 11 { &id[..11] } else { id }; + result.push_str(&format!(" [{}|{}]", truncated_id, version)); + } + + result.push('\n'); + } + + // Process children + let child_count = node.children.len(); + for (i, (_, child)) in node.children.iter().enumerate() { + let is_last_child = i == child_count - 1; + let mut new_last_stack = last_stack.to_vec(); + new_last_stack.push(is_last_child); + + add_tree_node_to_string(child, result, false, prefix, &new_last_stack); + } +} diff --git a/utils/src/legacy/env.rs b/utils/src/legacy/env.rs new file mode 100644 index 0000000..1834cd3 --- /dev/null +++ b/utils/src/legacy/env.rs @@ -0,0 +1,107 @@ +use std::path::PathBuf; + +/// Returns the current locale string based on environment variables. +/// +/// The function checks for locale settings in the following order: +/// 1. JV_LANG environment variable +/// 2. APP_LANG environment variable +/// 3. LANG environment variable (extracts base language before dot and replaces underscores with hyphens) +/// 4. Defaults to "en" if no locale environment variables are found +/// +/// # Returns +/// A String containing the detected locale code +pub fn current_locales() -> String { + if let Ok(lang) = std::env::var("JV_LANG") { + return lang; + } + + if let Ok(lang) = std::env::var("APP_LANG") { + return lang; + } + + if let Ok(lang) = std::env::var("LANG") { + if let Some(base_lang) = lang.split('.').next() { + return base_lang.replace('_', "-"); + } + return lang; + } + + "en".to_string() +} + +/// Checks if auto update is enabled based on environment variables. +/// +/// The function checks the JV_AUTO_UPDATE environment variable and compares +/// its value (after trimming and converting to lowercase) against known +/// positive and negative values. +/// +/// # Returns +/// `true` if the value matches "yes", "y", or "true" +/// `false` if the value matches "no", "n", or "false", or if the variable is not set +pub fn enable_auto_update() -> bool { + if let Ok(auto_update) = std::env::var("JV_AUTO_UPDATE") { + let normalized = auto_update.trim().to_lowercase(); + match normalized.as_str() { + "yes" | "y" | "true" => return true, + "no" | "n" | "false" => return false, + _ => {} + } + } + false +} + +/// Gets the auto update expiration time based on environment variables. +/// +/// The function checks the JV_OUTDATED_MINUTES environment variable. +/// Requires JV_AUTO_UPDATE to be enabled. +/// Next time the `jv` command is used, if the content is outdated, `jv update` will be automatically executed. +/// +/// # Returns +/// - When the set number is < 0, timeout-based update is disabled +/// - When the set number = 0, update runs every time (not recommended) +/// - When the set number > 0, update according to the specified time +/// - If not set or conversion error occurs, the default is -1 +pub fn auto_update_outdate() -> i64 { + if !enable_auto_update() { + return -1; + } + + match std::env::var("JV_OUTDATED_MINUTES") { + Ok(value) => match value.trim().parse::() { + Ok(num) => num, + Err(_) => -1, + }, + Err(_) => -1, + } +} + +/// Gets the default text editor based on environment variables. +/// +/// The function checks the JV_TEXT_EDITOR and EDITOR environment variables +/// and returns their values if they are set. If neither variable is set, +/// it returns "jvii" as the default editor. +/// +/// # Returns +/// A String containing the default text editor +pub async fn get_default_editor() -> String { + if let Ok(editor) = std::env::var("JV_TEXT_EDITOR") { + return editor; + } + + if let Ok(editor) = std::env::var("EDITOR") { + return editor; + } + + "jvii".to_string() +} + +/// Get temporary file path +pub fn current_tempfile_path(name: &str) -> Option { + dirs::config_local_dir().map(|path| { + if cfg!(target_os = "linux") { + path.join("jvcs").join(".temp").join(name) + } else { + path.join("JustEnoughVCS").join(".temp").join(name) + } + }) +} diff --git a/utils/src/legacy/fs.rs b/utils/src/legacy/fs.rs new file mode 100644 index 0000000..0050cf1 --- /dev/null +++ b/utils/src/legacy/fs.rs @@ -0,0 +1,40 @@ +pub async fn move_across_partitions( + source_path: impl AsRef, + dest_path: impl AsRef, +) -> Result<(), std::io::Error> { + let source_path = source_path.as_ref(); + let dest_path = dest_path.as_ref(); + if !source_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Source file does not exist", + )); + } + + if let Ok(()) = std::fs::rename(source_path, dest_path) { + return Ok(()); + } + + std::fs::copy(source_path, dest_path)?; + std::fs::remove_file(source_path)?; + + Ok(()) +} + +pub async fn copy_across_partitions( + source_path: impl AsRef, + dest_path: impl AsRef, +) -> Result<(), std::io::Error> { + let source_path = source_path.as_ref(); + let dest_path = dest_path.as_ref(); + if !source_path.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Source file does not exist", + )); + } + + std::fs::copy(source_path, dest_path)?; + + Ok(()) +} diff --git a/utils/src/legacy/globber.rs b/utils/src/legacy/globber.rs new file mode 100644 index 0000000..4d722db --- /dev/null +++ b/utils/src/legacy/globber.rs @@ -0,0 +1,279 @@ +use std::{io::Error, path::PathBuf, str::FromStr}; + +use just_fmt::fmt_path::fmt_path_str; + +use crate::legacy::globber::constants::{SPLIT_STR, get_base_dir_current}; + +pub struct Globber { + pattern: String, + base: PathBuf, + names: Vec, +} + +#[allow(dead_code)] +impl Globber { + pub fn new(pattern: String, base: PathBuf) -> Self { + Self { + pattern, + base, + names: Vec::new(), + } + } + + pub fn names(&self) -> Vec<&String> { + self.names.iter().collect() + } + + pub fn base(&self) -> &PathBuf { + &self.base + } + + pub fn into_names(self) -> Vec { + self.names + } + + pub fn paths(&self) -> Vec { + self.names.iter().map(|n| self.base.join(n)).collect() + } + + pub fn glob(mut self, get_names: F) -> Result + where + F: Fn(PathBuf) -> Vec, + { + let full_path = format!("{}{}{}", self.base.display(), SPLIT_STR, self.pattern); + + let (path, pattern) = if let Some(last_split) = full_path.rfind(SPLIT_STR) { + let (path_part, pattern_part) = full_path.split_at(last_split); + let mut path = path_part.to_string(); + if !path.ends_with(SPLIT_STR) { + path.push_str(SPLIT_STR); + } + let Ok(result) = fmt_path_str(&path) else { + return Err(Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid path: \"{}\"", &path), + )); + }; + (result, pattern_part[SPLIT_STR.len()..].to_string()) + } else { + (String::default(), full_path) + }; + + self.base = match PathBuf::from_str(&path) { + Ok(r) => r, + Err(_) => { + return Err(Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid path: \"{}\"", &path), + )); + } + }; + + let pattern = if pattern.is_empty() { + "*".to_string() + } else if pattern == "." { + "*".to_string() + } else if pattern.ends_with(SPLIT_STR) { + format!("{}*", pattern) + } else { + pattern + }; + + if !pattern.contains('*') && !pattern.contains('?') { + self.names = vec![pattern]; + return Ok(self); + } + + let mut collected = Vec::new(); + + collect_files(&path.into(), "./".to_string(), &mut collected, &get_names); + fn collect_files( + base: &PathBuf, + current: String, + file_names: &mut Vec, + get_names: &F, + ) where + F: Fn(PathBuf) -> Vec, + { + let current_path = if current.is_empty() { + base.clone() + } else { + base.join(¤t) + }; + + let items = get_names(current_path); + for item in items { + match item { + GlobItem::File(file_name) => { + let relative_path = { + fmt_path_str(format!("{}{}{}", current, SPLIT_STR, file_name)) + .unwrap_or_default() + }; + file_names.push(relative_path) + } + GlobItem::Directory(dir_name) => { + let new_current = { + fmt_path_str(format!("{}{}{}", current, SPLIT_STR, dir_name)) + .unwrap_or_default() + }; + collect_files(base, new_current, file_names, get_names); + } + } + } + } + + self.names = collected + .iter() + .filter_map(|name| match_pattern(name, &pattern)) + .collect(); + + Ok(self) + } +} + +fn match_pattern(name: &str, pattern: &str) -> Option { + if pattern.is_empty() { + return None; + } + + let name_chars: Vec = name.chars().collect(); + let pattern_chars: Vec = pattern.chars().collect(); + + let mut name_idx = 0; + let mut pattern_idx = 0; + let mut star_idx = -1; + let mut match_idx = -1; + + while name_idx < name_chars.len() { + if pattern_idx < pattern_chars.len() + && (pattern_chars[pattern_idx] == '?' + || pattern_chars[pattern_idx] == name_chars[name_idx]) + { + name_idx += 1; + pattern_idx += 1; + } else if pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { + star_idx = pattern_idx as i32; + match_idx = name_idx as i32; + pattern_idx += 1; + } else if star_idx != -1 { + pattern_idx = (star_idx + 1) as usize; + match_idx += 1; + name_idx = match_idx as usize; + } else { + return None; + } + } + + while pattern_idx < pattern_chars.len() && pattern_chars[pattern_idx] == '*' { + pattern_idx += 1; + } + + if pattern_idx == pattern_chars.len() { + Some(name.to_string()) + } else { + None + } +} + +impl> From for Globber { + fn from(pattern: T) -> Self { + let (base_dir, pattern) = get_base_dir_current(pattern.as_ref().to_string()); + Self::new(pattern, base_dir) + } +} + +#[derive(Debug, Clone, Hash)] +pub enum GlobItem { + File(String), + Directory(String), +} + +impl PartialEq for GlobItem { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (GlobItem::File(a), GlobItem::File(b)) => a == b, + (GlobItem::Directory(a), GlobItem::Directory(b)) => a == b, + _ => false, + } + } +} + +impl std::fmt::Display for GlobItem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + GlobItem::File(name) => write!(f, "{}", name), + GlobItem::Directory(name) => write!(f, "{}", name), + } + } +} + +impl Eq for GlobItem {} + +pub mod constants { + use std::{env::current_dir, path::PathBuf}; + + #[cfg(unix)] + pub(crate) const CURRENT_DIR_PREFIX: &str = "./"; + #[cfg(windows)] + pub(crate) const CURRENT_DIR_PREFIX: &str = ".\\"; + + #[cfg(unix)] + pub(crate) const USER_DIR_PREFIX: &str = "~"; + #[cfg(windows)] + pub(crate) const USER_DIR_PREFIX: &str = "~\\"; + + #[cfg(unix)] + pub(crate) const ROOT_DIR_PREFIX: &str = "/"; + #[cfg(windows)] + pub(crate) const ROOT_DIR_PREFIX: &str = "\\"; + + #[cfg(unix)] + pub(crate) const SPLIT_STR: &str = "/"; + #[cfg(windows)] + pub(crate) const SPLIT_STR: &str = "\\"; + + pub fn get_base_dir_current(input: String) -> (PathBuf, String) { + get_base_dir(input, current_dir().unwrap_or_default()) + } + + pub fn get_base_dir(input: String, current_dir: PathBuf) -> (PathBuf, String) { + if let Some(remaining) = input.strip_prefix(CURRENT_DIR_PREFIX) { + (current_dir, remaining.to_string()) + } else if let Some(remaining) = input.strip_prefix(USER_DIR_PREFIX) { + (dirs::home_dir().unwrap_or_default(), remaining.to_string()) + } else if let Some(remaining) = input.strip_prefix(ROOT_DIR_PREFIX) { + { + #[cfg(unix)] + { + (PathBuf::from(ROOT_DIR_PREFIX), remaining.to_string()) + } + #[cfg(windows)] + { + let current_drive = current_dir + .components() + .find_map(|comp| { + if let std::path::Component::Prefix(prefix_component) = comp { + Some(prefix_component) + } else { + None + } + }) + .and_then(|prefix_component| match prefix_component.kind() { + std::path::Prefix::Disk(drive_letter) + | std::path::Prefix::VerbatimDisk(drive_letter) => { + Some((drive_letter as char).to_string()) + } + _ => None, + }) + .unwrap_or_else(|| "C".to_string()); + ( + PathBuf::from(format!("{}:{}", current_drive, ROOT_DIR_PREFIX)), + remaining.to_string(), + ) + } + } + } else { + (current_dir, input) + } + } +} diff --git a/utils/src/legacy/input.rs b/utils/src/legacy/input.rs new file mode 100644 index 0000000..95d53cb --- /dev/null +++ b/utils/src/legacy/input.rs @@ -0,0 +1,151 @@ +use tokio::{fs, process::Command}; + +use crate::legacy::env::get_default_editor; + +/// Confirm the current operation +/// Waits for user input of 'y' or 'n' +pub async fn confirm_hint(text: impl Into) -> bool { + use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader}; + + let prompt = text.into().trim().to_string(); + + let mut stdout = io::stdout(); + let mut stdin = BufReader::new(io::stdin()); + + stdout + .write_all(prompt.as_bytes()) + .await + .expect("Failed to write prompt"); + stdout.flush().await.expect("Failed to flush stdout"); + + let mut input = String::new(); + stdin + .read_line(&mut input) + .await + .expect("Failed to read input"); + + input.trim().eq_ignore_ascii_case("y") +} + +/// Confirm the current operation, or execute a closure if rejected +/// Waits for user input of 'y' or 'n' +/// If 'n' is entered, executes the provided closure and returns false +pub async fn confirm_hint_or(text: impl Into, on_reject: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if !confirmed { + on_reject(); + } + confirmed +} + +/// Confirm the current operation, and execute a closure if confirmed +/// Waits for user input of 'y' or 'n' +/// If 'y' is entered, executes the provided closure and returns true +pub async fn confirm_hint_then(text: impl Into, on_confirm: F) -> bool +where + F: FnOnce(), +{ + let confirmed = confirm_hint(text).await; + if confirmed { + on_confirm(); + } + confirmed +} + +/// Input text using the system editor +/// Opens the system editor (from EDITOR environment variable) with default text in a cache file, +/// then reads back the modified content after the editor closes, removing comment lines +pub async fn input_with_editor( + default_text: impl AsRef, + cache_file: impl AsRef, + comment_char: impl AsRef, +) -> Result { + input_with_editor_cutsom( + default_text, + cache_file, + comment_char, + get_default_editor().await, + ) + .await +} + +pub async fn input_with_editor_cutsom( + default_text: impl AsRef, + cache_file: impl AsRef, + comment_char: impl AsRef, + editor: String, +) -> Result { + let cache_path = cache_file.as_ref(); + let default_content = default_text.as_ref(); + let comment_prefix = comment_char.as_ref(); + + // Write default text to cache file + fs::write(cache_path, default_content).await?; + + // Open editor with cache file + let status = Command::new(editor).arg(cache_path).status().await?; + + if !status.success() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Editor exited with non-zero status", + )); + } + + // Read the modified content + let content = fs::read_to_string(cache_path).await?; + + // Remove comment lines and trim + let processed_content: String = content + .lines() + .filter_map(|line| { + let trimmed = line.trim(); + if trimmed.starts_with(comment_prefix) { + None + } else { + Some(line) + } + }) + .collect::>() + .join("\n"); + + // Delete the cache file + let _ = fs::remove_file(cache_path).await; + + Ok(processed_content) +} + +/// Show text using the system pager (less) +/// Opens the system pager (less) with the given text content written to the specified file +/// If less is not found, directly outputs the content to stdout +pub async fn show_in_pager( + content: impl AsRef, + cache_file: impl AsRef, +) -> Result<(), std::io::Error> { + let content_str = content.as_ref(); + let cache_path = cache_file.as_ref(); + + // Write content to cache file + fs::write(cache_path, content_str).await?; + + // Try to use less first + let status = Command::new("less").arg(cache_path).status().await; + + match status { + Ok(status) if status.success() => Ok(()), + _ => { + // If less failed, output directly to stdout + use tokio::io::{self, AsyncWriteExt}; + let mut stdout = io::stdout(); + stdout + .write_all(content_str.as_bytes()) + .await + .expect("Failed to write content"); + stdout.flush().await.expect("Failed to flush stdout"); + Ok(()) + } + } +} diff --git a/utils/src/legacy/levenshtein_distance.rs b/utils/src/legacy/levenshtein_distance.rs new file mode 100644 index 0000000..6bdb7e7 --- /dev/null +++ b/utils/src/legacy/levenshtein_distance.rs @@ -0,0 +1,34 @@ +use std::cmp::min; + +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let a_chars: Vec = a.chars().collect(); + let b_chars: Vec = b.chars().collect(); + let a_len = a_chars.len(); + let b_len = b_chars.len(); + + if a_len == 0 { + return b_len; + } + if b_len == 0 { + return a_len; + } + + let mut dp = vec![vec![0; b_len + 1]; a_len + 1]; + + for (i, row) in dp.iter_mut().enumerate() { + row[0] = i; + } + + for (j, cell) in dp[0].iter_mut().enumerate() { + *cell = j; + } + + for (i, a_char) in a_chars.iter().enumerate() { + for (j, b_char) in b_chars.iter().enumerate() { + let cost = if a_char == b_char { 0 } else { 1 }; + dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost)); + } + } + + dp[a_len][b_len] +} diff --git a/utils/src/legacy/logger.rs b/utils/src/legacy/logger.rs new file mode 100644 index 0000000..1bc96c1 --- /dev/null +++ b/utils/src/legacy/logger.rs @@ -0,0 +1,86 @@ +use std::path::Path; + +use colored::Colorize; +use env_logger::{Builder, Target}; +use just_enough_vcs::lib::data::vault::vault_config::LoggerLevel; +use just_fmt::fmt_path::fmt_path; +use log::{Level, LevelFilter}; + +pub fn build_env_logger(log_path: impl AsRef, logger_level: LoggerLevel) { + use std::io::{self, Write}; + + struct MultiWriter { + a: A, + b: B, + } + + impl MultiWriter { + fn new(a: A, b: B) -> Self { + Self { a, b } + } + } + + impl Write for MultiWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let _ = self.a.write(buf); + self.b.write(buf) + } + + fn flush(&mut self) -> io::Result<()> { + let _ = self.a.flush(); + self.b.flush() + } + } + + let log_path = { + let path = log_path.as_ref(); + let Ok(path) = fmt_path(path) else { + eprintln!( + "Build logger failed: {} is not a vaild path.", + path.display() + ); + return; + }; + path + }; + + let mut builder = Builder::new(); + + let log_format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record| { + let now = chrono::Local::now(); + + let level_style = match record.level() { + Level::Error => record.args().to_string().red().bold(), + Level::Warn => record.args().to_string().yellow().bold(), + Level::Info => record.args().to_string().white(), + Level::Debug => record.args().to_string().white(), + Level::Trace => record.args().to_string().cyan(), + }; + + writeln!( + buf, + "{} {}", + now.format("%H:%M:%S") + .to_string() + .truecolor(105, 105, 105) + .bold(), + level_style + ) + }; + + let log_file = std::fs::File::create(log_path).expect("Failed to create log file"); + let combined_target = Target::Pipe(Box::new(MultiWriter::new(std::io::stdout(), log_file))); + + let level = match logger_level { + LoggerLevel::Debug => LevelFilter::Debug, + LoggerLevel::Trace => LevelFilter::Trace, + LoggerLevel::Info => LevelFilter::Info, + }; + + builder + .format(log_format) + .filter(None, level.clone()) + .filter_module("just_enough_vcs", level) + .target(combined_target) + .init(); +} diff --git a/utils/src/legacy/push_version.rs b/utils/src/legacy/push_version.rs new file mode 100644 index 0000000..6da9039 --- /dev/null +++ b/utils/src/legacy/push_version.rs @@ -0,0 +1,30 @@ +pub fn push_version(current_version: impl Into) -> Option { + let version_str = current_version.into(); + let parts: Vec<&str> = version_str.split('.').collect(); + + if parts.len() != 3 { + return None; + } + + let major: Result = parts[0].parse(); + let minor: Result = parts[1].parse(); + let patch: Result = parts[2].parse(); + + if let (Ok(mut major), Ok(mut minor), Ok(mut patch)) = (major, minor, patch) { + patch += 1; + + if patch > 99 { + patch = 0; + minor += 1; + + if minor > 99 { + minor = 0; + major += 1; + } + } + + Some(format!("{}.{}.{}", major, minor, patch)) + } else { + None + } +} diff --git a/utils/src/legacy/socket_addr_helper.rs b/utils/src/legacy/socket_addr_helper.rs new file mode 100644 index 0000000..29ccd9f --- /dev/null +++ b/utils/src/legacy/socket_addr_helper.rs @@ -0,0 +1,194 @@ +use std::net::SocketAddr; +use tokio::net::lookup_host; + +/// Helper function to parse a string into a SocketAddr with optional default port +pub async fn get_socket_addr( + address_str: impl AsRef, + default_port: u16, +) -> Result { + let address = address_str.as_ref().trim(); + + // Return error if input is empty after trimming + if address.is_empty() { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Empty address string", + )); + } + + // Check if the address contains a port + if let Some((host, port_str)) = parse_host_and_port(address) { + let port = port_str.parse::().map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Invalid port number '{}': {}", port_str, e), + ) + })?; + + return resolve_to_socket_addr(host, port).await; + } + + // No port specified, use default port + resolve_to_socket_addr(address, default_port).await +} + +/// Parse host and port from address string +fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { + if address.starts_with('[') + && let Some(close_bracket) = address.find(']') + && close_bracket + 1 < address.len() + && address.as_bytes()[close_bracket + 1] == b':' + { + let host = &address[1..close_bracket]; + let port = &address[close_bracket + 2..]; + return Some((host, port)); + } + + // Handle IPv4 addresses and hostnames with ports + if let Some(colon_pos) = address.rfind(':') { + // Check if this is not part of an IPv6 address without brackets + if !address.contains('[') && !address.contains(']') { + let host = &address[..colon_pos]; + let port = &address[colon_pos + 1..]; + + // Basic validation to avoid false positives + if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) { + return Some((host, port)); + } + } + } + + None +} + +/// Resolve host to SocketAddr, handling both IP addresses and domain names +async fn resolve_to_socket_addr(host: &str, port: u16) -> Result { + // First try to parse as IP address (IPv4 or IPv6) + if let Ok(ip_addr) = host.parse() { + return Ok(SocketAddr::new(ip_addr, port)); + } + + // If it's not a valid IP address, treat it as a domain name and perform DNS lookup + let lookup_addr = format!("{}:{}", host, port); + let mut addrs = lookup_host(&lookup_addr).await?; + + if let Some(addr) = addrs.next() { + Ok(addr) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("Could not resolve host '{}'", host), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_ipv4_with_port() { + let result = get_socket_addr("127.0.0.1:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "127.0.0.1"); + } + + #[tokio::test] + async fn test_ipv4_without_port() { + let result = get_socket_addr("192.168.1.1", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "192.168.1.1"); + } + + #[tokio::test] + async fn test_ipv6_with_port() { + let result = get_socket_addr("[::1]:8080", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_ipv6_without_port() { + let result = get_socket_addr("[::1]", 443).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + assert_eq!(addr.ip().to_string(), "::1"); + } + + #[tokio::test] + async fn test_invalid_port() { + let result = get_socket_addr("127.0.0.1:99999", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_empty_string() { + let result = get_socket_addr("", 80).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_whitespace_trimming() { + let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await; + assert!(result.is_ok()); + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + } + + #[tokio::test] + async fn test_domain_name_with_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost:8080", 80).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 8080); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_domain_name_without_port() { + // This test will only pass if localhost resolves + let result = get_socket_addr("localhost", 443).await; + if result.is_ok() { + let addr = result.unwrap(); + assert_eq!(addr.port(), 443); + // localhost should resolve to 127.0.0.1 or ::1 + assert!(addr.ip().is_loopback()); + } + } + + #[tokio::test] + async fn test_parse_host_and_port() { + // IPv4 with port + assert_eq!( + parse_host_and_port("192.168.1.1:8080"), + Some(("192.168.1.1", "8080")) + ); + + // IPv6 with port + assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080"))); + + // Hostname with port + assert_eq!( + parse_host_and_port("example.com:443"), + Some(("example.com", "443")) + ); + + // No port + assert_eq!(parse_host_and_port("192.168.1.1"), None); + assert_eq!(parse_host_and_port("example.com"), None); + + // Invalid cases + assert_eq!(parse_host_and_port(":"), None); + assert_eq!(parse_host_and_port("192.168.1.1:"), None); + } +} diff --git a/utils/src/levenshtein_distance.rs b/utils/src/levenshtein_distance.rs deleted file mode 100644 index 6bdb7e7..0000000 --- a/utils/src/levenshtein_distance.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::cmp::min; - -pub fn levenshtein_distance(a: &str, b: &str) -> usize { - let a_chars: Vec = a.chars().collect(); - let b_chars: Vec = b.chars().collect(); - let a_len = a_chars.len(); - let b_len = b_chars.len(); - - if a_len == 0 { - return b_len; - } - if b_len == 0 { - return a_len; - } - - let mut dp = vec![vec![0; b_len + 1]; a_len + 1]; - - for (i, row) in dp.iter_mut().enumerate() { - row[0] = i; - } - - for (j, cell) in dp[0].iter_mut().enumerate() { - *cell = j; - } - - for (i, a_char) in a_chars.iter().enumerate() { - for (j, b_char) in b_chars.iter().enumerate() { - let cost = if a_char == b_char { 0 } else { 1 }; - dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost)); - } - } - - dp[a_len][b_len] -} diff --git a/utils/src/lib.rs b/utils/src/lib.rs index ef56189..ca2be9c 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -1,10 +1,6 @@ pub mod display; -pub mod env; -pub mod fs; -pub mod globber; -pub mod input; -pub mod lazy_macros; -pub mod levenshtein_distance; -pub mod logger; -pub mod push_version; -pub mod socket_addr_helper; +pub mod macros; +pub mod math; + +// Legacy +pub mod legacy; diff --git a/utils/src/logger.rs b/utils/src/logger.rs deleted file mode 100644 index 1bc96c1..0000000 --- a/utils/src/logger.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::path::Path; - -use colored::Colorize; -use env_logger::{Builder, Target}; -use just_enough_vcs::lib::data::vault::vault_config::LoggerLevel; -use just_fmt::fmt_path::fmt_path; -use log::{Level, LevelFilter}; - -pub fn build_env_logger(log_path: impl AsRef, logger_level: LoggerLevel) { - use std::io::{self, Write}; - - struct MultiWriter { - a: A, - b: B, - } - - impl MultiWriter { - fn new(a: A, b: B) -> Self { - Self { a, b } - } - } - - impl Write for MultiWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let _ = self.a.write(buf); - self.b.write(buf) - } - - fn flush(&mut self) -> io::Result<()> { - let _ = self.a.flush(); - self.b.flush() - } - } - - let log_path = { - let path = log_path.as_ref(); - let Ok(path) = fmt_path(path) else { - eprintln!( - "Build logger failed: {} is not a vaild path.", - path.display() - ); - return; - }; - path - }; - - let mut builder = Builder::new(); - - let log_format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record| { - let now = chrono::Local::now(); - - let level_style = match record.level() { - Level::Error => record.args().to_string().red().bold(), - Level::Warn => record.args().to_string().yellow().bold(), - Level::Info => record.args().to_string().white(), - Level::Debug => record.args().to_string().white(), - Level::Trace => record.args().to_string().cyan(), - }; - - writeln!( - buf, - "{} {}", - now.format("%H:%M:%S") - .to_string() - .truecolor(105, 105, 105) - .bold(), - level_style - ) - }; - - let log_file = std::fs::File::create(log_path).expect("Failed to create log file"); - let combined_target = Target::Pipe(Box::new(MultiWriter::new(std::io::stdout(), log_file))); - - let level = match logger_level { - LoggerLevel::Debug => LevelFilter::Debug, - LoggerLevel::Trace => LevelFilter::Trace, - LoggerLevel::Info => LevelFilter::Info, - }; - - builder - .format(log_format) - .filter(None, level.clone()) - .filter_module("just_enough_vcs", level) - .target(combined_target) - .init(); -} diff --git a/utils/src/macros.rs b/utils/src/macros.rs new file mode 100644 index 0000000..f1cb75e --- /dev/null +++ b/utils/src/macros.rs @@ -0,0 +1,14 @@ +/// A macro for creating a `Vec` from string literals. +/// +/// # Examples +/// ``` +/// # use cli_utils::string_vec; +/// let v = string_vec!["hello", "world"]; +/// assert_eq!(v, vec!["hello".to_string(), "world".to_string()]); +/// ``` +#[macro_export] +macro_rules! string_vec { + ($($elem:expr),* $(,)?) => { + vec![$($elem.to_string()),*] + }; +} diff --git a/utils/src/math.rs b/utils/src/math.rs new file mode 100644 index 0000000..42a44a1 --- /dev/null +++ b/utils/src/math.rs @@ -0,0 +1 @@ +pub mod levenshtein_distance; diff --git a/utils/src/math/levenshtein_distance.rs b/utils/src/math/levenshtein_distance.rs new file mode 100644 index 0000000..98caa20 --- /dev/null +++ b/utils/src/math/levenshtein_distance.rs @@ -0,0 +1,38 @@ +use std::cmp::min; + +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let a_len = a.chars().count(); + let b_len = b.chars().count(); + + if a_len == 0 { + return b_len; + } + if b_len == 0 { + return a_len; + } + + let mut prev_row: Vec = (0..=b_len).collect(); + let mut curr_row = vec![0; b_len + 1]; + + let mut a_chars = a.chars(); + + for i in 1..=a_len { + let a_char = a_chars.next().unwrap(); + curr_row[0] = i; + + let mut b_chars = b.chars(); + for j in 1..=b_len { + let b_char = b_chars.next().unwrap(); + + let cost = if a_char == b_char { 0 } else { 1 }; + curr_row[j] = min( + prev_row[j] + 1, + min(curr_row[j - 1] + 1, prev_row[j - 1] + cost), + ); + } + + std::mem::swap(&mut prev_row, &mut curr_row); + } + + prev_row[b_len] +} diff --git a/utils/src/push_version.rs b/utils/src/push_version.rs deleted file mode 100644 index 6da9039..0000000 --- a/utils/src/push_version.rs +++ /dev/null @@ -1,30 +0,0 @@ -pub fn push_version(current_version: impl Into) -> Option { - let version_str = current_version.into(); - let parts: Vec<&str> = version_str.split('.').collect(); - - if parts.len() != 3 { - return None; - } - - let major: Result = parts[0].parse(); - let minor: Result = parts[1].parse(); - let patch: Result = parts[2].parse(); - - if let (Ok(mut major), Ok(mut minor), Ok(mut patch)) = (major, minor, patch) { - patch += 1; - - if patch > 99 { - patch = 0; - minor += 1; - - if minor > 99 { - minor = 0; - major += 1; - } - } - - Some(format!("{}.{}.{}", major, minor, patch)) - } else { - None - } -} diff --git a/utils/src/socket_addr_helper.rs b/utils/src/socket_addr_helper.rs deleted file mode 100644 index 29ccd9f..0000000 --- a/utils/src/socket_addr_helper.rs +++ /dev/null @@ -1,194 +0,0 @@ -use std::net::SocketAddr; -use tokio::net::lookup_host; - -/// Helper function to parse a string into a SocketAddr with optional default port -pub async fn get_socket_addr( - address_str: impl AsRef, - default_port: u16, -) -> Result { - let address = address_str.as_ref().trim(); - - // Return error if input is empty after trimming - if address.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "Empty address string", - )); - } - - // Check if the address contains a port - if let Some((host, port_str)) = parse_host_and_port(address) { - let port = port_str.parse::().map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - format!("Invalid port number '{}': {}", port_str, e), - ) - })?; - - return resolve_to_socket_addr(host, port).await; - } - - // No port specified, use default port - resolve_to_socket_addr(address, default_port).await -} - -/// Parse host and port from address string -fn parse_host_and_port(address: &str) -> Option<(&str, &str)> { - if address.starts_with('[') - && let Some(close_bracket) = address.find(']') - && close_bracket + 1 < address.len() - && address.as_bytes()[close_bracket + 1] == b':' - { - let host = &address[1..close_bracket]; - let port = &address[close_bracket + 2..]; - return Some((host, port)); - } - - // Handle IPv4 addresses and hostnames with ports - if let Some(colon_pos) = address.rfind(':') { - // Check if this is not part of an IPv6 address without brackets - if !address.contains('[') && !address.contains(']') { - let host = &address[..colon_pos]; - let port = &address[colon_pos + 1..]; - - // Basic validation to avoid false positives - if !host.is_empty() && !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) { - return Some((host, port)); - } - } - } - - None -} - -/// Resolve host to SocketAddr, handling both IP addresses and domain names -async fn resolve_to_socket_addr(host: &str, port: u16) -> Result { - // First try to parse as IP address (IPv4 or IPv6) - if let Ok(ip_addr) = host.parse() { - return Ok(SocketAddr::new(ip_addr, port)); - } - - // If it's not a valid IP address, treat it as a domain name and perform DNS lookup - let lookup_addr = format!("{}:{}", host, port); - let mut addrs = lookup_host(&lookup_addr).await?; - - if let Some(addr) = addrs.next() { - Ok(addr) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Could not resolve host '{}'", host), - )) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_ipv4_with_port() { - let result = get_socket_addr("127.0.0.1:8080", 80).await; - assert!(result.is_ok()); - let addr = result.unwrap(); - assert_eq!(addr.port(), 8080); - assert_eq!(addr.ip().to_string(), "127.0.0.1"); - } - - #[tokio::test] - async fn test_ipv4_without_port() { - let result = get_socket_addr("192.168.1.1", 443).await; - assert!(result.is_ok()); - let addr = result.unwrap(); - assert_eq!(addr.port(), 443); - assert_eq!(addr.ip().to_string(), "192.168.1.1"); - } - - #[tokio::test] - async fn test_ipv6_with_port() { - let result = get_socket_addr("[::1]:8080", 80).await; - assert!(result.is_ok()); - let addr = result.unwrap(); - assert_eq!(addr.port(), 8080); - assert_eq!(addr.ip().to_string(), "::1"); - } - - #[tokio::test] - async fn test_ipv6_without_port() { - let result = get_socket_addr("[::1]", 443).await; - assert!(result.is_ok()); - let addr = result.unwrap(); - assert_eq!(addr.port(), 443); - assert_eq!(addr.ip().to_string(), "::1"); - } - - #[tokio::test] - async fn test_invalid_port() { - let result = get_socket_addr("127.0.0.1:99999", 80).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_empty_string() { - let result = get_socket_addr("", 80).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_whitespace_trimming() { - let result = get_socket_addr(" 127.0.0.1:8080 ", 80).await; - assert!(result.is_ok()); - let addr = result.unwrap(); - assert_eq!(addr.port(), 8080); - } - - #[tokio::test] - async fn test_domain_name_with_port() { - // This test will only pass if localhost resolves - let result = get_socket_addr("localhost:8080", 80).await; - if result.is_ok() { - let addr = result.unwrap(); - assert_eq!(addr.port(), 8080); - // localhost should resolve to 127.0.0.1 or ::1 - assert!(addr.ip().is_loopback()); - } - } - - #[tokio::test] - async fn test_domain_name_without_port() { - // This test will only pass if localhost resolves - let result = get_socket_addr("localhost", 443).await; - if result.is_ok() { - let addr = result.unwrap(); - assert_eq!(addr.port(), 443); - // localhost should resolve to 127.0.0.1 or ::1 - assert!(addr.ip().is_loopback()); - } - } - - #[tokio::test] - async fn test_parse_host_and_port() { - // IPv4 with port - assert_eq!( - parse_host_and_port("192.168.1.1:8080"), - Some(("192.168.1.1", "8080")) - ); - - // IPv6 with port - assert_eq!(parse_host_and_port("[::1]:8080"), Some(("::1", "8080"))); - - // Hostname with port - assert_eq!( - parse_host_and_port("example.com:443"), - Some(("example.com", "443")) - ); - - // No port - assert_eq!(parse_host_and_port("192.168.1.1"), None); - assert_eq!(parse_host_and_port("example.com"), None); - - // Invalid cases - assert_eq!(parse_host_and_port(":"), None); - assert_eq!(parse_host_and_port("192.168.1.1:"), None); - } -} -- cgit