diff --git a/Cargo.lock b/Cargo.lock index 5e069af7..d572a1f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,25 +19,30 @@ dependencies = [ [[package]] name = "aes" -version = "0.7.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", "cpufeatures", - "opaque-debug", ] [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -88,7 +93,7 @@ checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -228,26 +233,26 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.5", ] [[package]] name = "cipher" -version = "0.3.0" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "generic-array", + "crypto-common", + "inout", ] [[package]] @@ -290,7 +295,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -495,7 +500,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn", + "syn 1.0.109", ] [[package]] @@ -512,7 +517,7 @@ checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -536,7 +541,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.9.3", - "syn", + "syn 1.0.109", ] [[package]] @@ -547,7 +552,7 @@ checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" dependencies = [ "darling_core", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -568,7 +573,7 @@ checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -581,7 +586,7 @@ dependencies = [ "derive_builder_core", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -593,7 +598,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -844,7 +849,7 @@ dependencies = [ [[package]] name = "fm-tui" -version = "0.1.21" +version = "0.1.22" dependencies = [ "anyhow", "chrono", @@ -856,6 +861,7 @@ dependencies = [ "futures 0.3.27", "gag", "indicatif", + "lazy_static", "log", "log4rs", "nvim-rs", @@ -957,7 +963,7 @@ checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1157,6 +1163,15 @@ dependencies = [ "vt100", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1371,9 +1386,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memmap2" @@ -1473,16 +1488,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.15" @@ -1581,12 +1586,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - [[package]] name = "ordered-float" version = "2.10.0" @@ -1692,9 +1691,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" @@ -1767,7 +1766,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -1790,9 +1789,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1808,9 +1807,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1930,13 +1929,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-automata", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", ] [[package]] @@ -1945,6 +1956,12 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -2104,7 +2121,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2319,7 +2336,7 @@ dependencies = [ "quote", "serde", "serde_derive", - "syn", + "syn 1.0.109", ] [[package]] @@ -2335,7 +2352,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1 0.6.1", - "syn", + "syn 1.0.109", ] [[package]] @@ -2387,7 +2404,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.109", ] [[package]] @@ -2407,6 +2424,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syntect" version = "5.0.0" @@ -2421,7 +2449,7 @@ dependencies = [ "once_cell", "onig", "plist", - "regex-syntax", + "regex-syntax 0.6.28", "serde", "serde_derive", "serde_json", @@ -2501,22 +2529,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.37", ] [[package]] @@ -2540,17 +2568,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.2.27" @@ -2613,7 +2630,7 @@ dependencies = [ "proc-macro2", "quote", "standback", - "syn", + "syn 1.0.109", ] [[package]] @@ -2679,7 +2696,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -2875,12 +2892,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2908,7 +2919,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -2930,7 +2941,7 @@ checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3071,13 +3082,13 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -3086,7 +3097,7 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.1", ] [[package]] @@ -3095,13 +3106,28 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.1", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm 0.42.1", + "windows_x86_64_msvc 0.42.1", +] + +[[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]] @@ -3110,42 +3136,84 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[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_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[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_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +[[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_gnullvm" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[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_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "x11-clipboard" version = "0.7.0" @@ -3197,9 +3265,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.4" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" [[package]] name = "yaml-rust" @@ -3212,9 +3280,9 @@ dependencies = [ [[package]] name = "zip" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "aes", "byteorder", diff --git a/Cargo.toml b/Cargo.toml index 7dda3801..bbe9b5b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fm-tui" -version = "0.1.21" +version = "0.1.22" authors = ["Quentin Konieczko "] edition = "2021" license-file = "LICENSE.txt" @@ -30,7 +30,7 @@ fs_extra = "1.2.0" [dependencies] anyhow = "1.0.28" -chrono = "0.4.22" +chrono = "0.4.31" clap = { version = "4.0.2", features = ["derive"] } content_inspector = "0.2.4" copypasta = "0.8.1" @@ -39,8 +39,9 @@ fs_extra = "1.2.0" futures = "0.3" gag = "1.0.0" indicatif = { version = "0.17.1", features= ["in_memory"] } +lazy_static = "1.4.0" log = { version = "0.4.0", features = ["std"] } -log4rs = {version = "1.2.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] } +log4rs = { version = "1.2.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller"] } nvim-rs = { version = "0.3", features = ["use_tokio"] } pathdiff = "0.2.1" pdf-extract = "0.6.4" diff --git a/config_files/fm/config.yaml b/config_files/fm/config.yaml index 1f9fb410..a78026cd 100644 --- a/config_files/fm/config.yaml +++ b/config_files/fm/config.yaml @@ -10,7 +10,7 @@ colors: socket: cyan symlink: magenta keys: - 'esc': ModeNormal + 'esc': ResetMode 'up': MoveUp 'down': MoveDown 'left': MoveLeft @@ -65,7 +65,7 @@ keys: 'ctrl-f': FuzzyFind 'ctrl-g': Shortcut 'ctrl-p': CopyFilepath - 'ctrl-q': ModeNormal + 'ctrl-q': ResetMode 'ctrl-r': RefreshView 'alt-d': DragNDrop 'alt-e': ToggleDisplayFull diff --git a/config_files/fm/logging_config.yaml b/config_files/fm/logging_config.yaml index b8ddf211..aad9f10e 100644 --- a/config_files/fm/logging_config.yaml +++ b/config_files/fm/logging_config.yaml @@ -9,10 +9,7 @@ appenders: kind: size limit: 50kb roller: - kind: fixed_window - base: 1 - count: 10 - pattern: "$ENV{HOME}/.config/fm/log/fm{}.log" + kind: delete action_logger: kind: rolling_file path: "$ENV{HOME}/.config/fm/log/action_logger.log" diff --git a/development.md b/development.md index 6ba0f4bb..d6126715 100644 --- a/development.md +++ b/development.md @@ -453,8 +453,6 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally. - [x] mocp go to song: `mocp -Q %file` with alt+enter (lack of a better keybinding) - [x] display openers in help -## Current dev - ### Version 0.1.21 - [x] more shortcuts like `nnn` : `\` root, @: start @@ -487,25 +485,97 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally. - [x] event: linked to an Action, same name - [x] exec: linked to an executable mode, same name - [x] every helper should be moved outside the struct -- [x] FIX: impossible to compile on MacOs since to `sysinfo::Disk` only implement `PartialEq` on linux. - Can't test MacOs compilation since I don't own a mac... +- [x] FIX: impossible to compile on MacOs since to `sysinfo::Disk` only implement `PartialEq` on linux. + Can't test MacOs compilation since I don't own a mac... - [x] FIX: incompatible config files between versions crashes the app. - [x] FIX: Help string susbtitution aren't aligned properly +- [x] FIX: exiting preview doesn't refresh +- [x] Mode should know if a refresh is required after leaving them. + +## Current dev + +### Version 0.1.22 + +- [x] FIX: copying 0 bytes crash progress bar thread +- [x] FIX: refresh users (from tab) reset the selection in pathcontent. +- [x] FIX: when switching from single to dual pane, sort and position are lost +- [x] FIX: tree mode: move down from bottom crashes +- [x] FIX: inxi --full or inxi -F hangs. Use inxi -v 2 instead +- [x] allow shell expansion (~ -> /home/user) in goto mode +- [x] FIX: mode CHMOD reset the file index +- [x] better display of selected tab. Reverse the colors in the first line +- [x] display a message when trash is empty in trash explore mode (alt-o) +- [x] display last executed action (use a string as message) +- [x] FIX: vertical resize to a smaller window : files expand to the last line and message are overwritten +- [x] FIX: open a secondary window and messages are overwritten by files. Don't display messages... +- [x] FIX: clippy term_manager::windmain has too many arguments. Create a struct holding params for winmain +- [x] NeedConfirmation modes are responsible for their confirmation message +- [x] Use Alt+r to remote mount with sshfs. + - request `username hostname remotepath` in one stroke, + - execute `sshfs remote_user@hostname:remote_path current_path` which will mount the remote path in current path +- [x] FIX: search keybindings don't work. Need to trim a string. +- [x] FIX: archive depends on CWD and will crash if it's not set properly (ie. change tab, move, change tab, compress) +- [x] use memory and not disk to read last line of logs. +- [x] mocp clear playlist with ctrl+x +- [x] FIX: MOCP print error message on screen +- [x] cryptdevice requires lsblk & cryptdevice. Display a message if not installed +- [x] mocp must be installed to run relatives commands +- [x] nitrogen must be installed to set a wallpaper +- [x] mediainfo must be installed to preview a media file with it +- [x] ueberzug must be installed to preview images & font files with it +- [x] pandoc must be installed to preview doc (.doc, .odb) +- [x] jupyter must be installed to preview .ipynb notebooks. +- [x] isoinfo must be installed to preview .iso files +- [x] diff must be installed to preview a diff of 2 files +- [x] git muse be installed to display a git status string +- [x] inform user if file already exits when creating / renaming +- [x] factorise new file & new dir +- [x] metadata in tree mode. Toggle like display full with alt-e +- [x] FIX: pagedown may select a file outside window without scrolling to it +- [x] FIX: multiple scrolling bugs. It should be smooth in every context +- [x] FIX: after scrolling left click doesn't select the correct file +- [x] FIX: page down when few files in current path screw the display +- [x] remove doublons from shortcut (Ctrl+g) "goto mode" +- [x] FIX: scrolling isn't smooth anymore +- [x] InputSimple is responsible of its help lines +- [x] Preview epub. Requires pandoc. +- [x] FIX: symlink aren't displayed at all. + - Improve broken symlink detection and display + - Use `symlink_metadata` to avoid following symlink in tree mode, which may cause recursive filetree + - Don't display symlink destination in tree mode since it clutters the display + - Use a different configurable color for broken symlink +- [x] display selected file in first line +- [x] FIX: sort by size use wrong value and order badly 2B > 1M +- [x] refactor copy move. CopyOrMove is responsible for its setup. +- [x] refactor main. Split setup, exit and main loop. +- [x] refactor main. Use a struct responsible for setup, update, display and quit. +- [x] preview fonts, svg & video thumbnail + - video thumbnail requires ffmpeg + - fonts preview otf not supported + - fonts preview requires fontimage + - svg preview requires rsvg-convert +- [x] preview for special files : + - [x] block device using lsblk + - [x] socket using ss + - [x] fifo & chardevice `lsof path` +- [x] size for char & block device [exa](https://github.com/ogham/exa/blob/fb05c421ae98e076989eb6e8b1bcf42c07c1d0fe/src/fs/file.rs#L331) +- [x] use a struct for ColumnSize +- [x] FIX: goto mode from tree node with a selected file crashes the application +- [x] Not accessible file in tree mode crashes the application +- [x] Look for nvim listen address in `ss -l` output ## TODO +- [ ] while second window is opened, if the selection is below half screen, it's not shown anymore. + Scroll to second element if needed - [ ] remote control - - [x] filepicker - requires the nvim-remote rust crate installed - [ ] listen to stdin (rcv etc.) - [ ] follow change directory - [ ] when called from a file buffer in nvim, open with this file selected - [ ] nvim plugin - set a serverstart with a listenaddress, send it to fm - https://github.com/KillTheMule/nvim-rs/blob/master/examples/basic.rs - https://neovim.io/doc/user/api.html - - [ ] $NVIM_LISTEN_ADDRESS isn't always set on nbim startup ; can be set from nvim before running... then sent to fm with some args - - [ ] args read correctly, use NVIM_LISTEN_ADDRESS if args is sent - [ ] display / event separation. use async and message passing between coroutines @@ -516,12 +586,12 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally. - [ ] read events from stdin ? can't be done from tuikit. Would require another thread ? - [ ] pushbullet ? - - [ ] update the animation - - [ ] exec multiple flagged files - - [ ] shell menu +- [ ] update the animation +- [ ] exec multiple flagged files +- [ ] shell menu - - [ ] allow non tui like wttr, diff, bat, tail -n etc. - - [ ] more options like "use flagged files" for diff + - [ ] allow non tui like wttr, diff, bat, tail -n etc. + - [ ] more options like "use flagged files" for diff - [ ] build option to force reset of config file, warn the user at first start - [ ] optionable "plugin" started from config file. Would require every option to be `Option` and may cause problems with the borrow checker. @@ -534,12 +604,6 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally. - [ ] list non mounted devices, list all mount points - [ ] act on them -- [ ] sub window / menu for completion / selection. - - 1. enter selectable mode - 2. chose an action - 3. confirm - - [ ] Version 0.1.50 : safety & memory usage - [ ] there's a memory leak somewhere @@ -566,7 +630,9 @@ New view: Tree ! Toggle with 't', fold with 'z'. Navigate normally. ## BUGS -- [ ] when opening a file with rifle opener into nvim and closing, the terminal hangs +- [ ] creates $ENV{HOME} folders everywhere - + a new version of log4rs seems to solve this, it's not deplayed to crates.io yet +- [ ] tree mode : index are offset by one ## Won't do diff --git a/src/action_map.rs b/src/action_map.rs index 0ab9c856..cd829ee0 100644 --- a/src/action_map.rs +++ b/src/action_map.rs @@ -50,6 +50,7 @@ pub enum ActionMap { MarksNew, MediaInfo, MocpAddToPlayList, + MocpClearPlaylist, MocpGoToSong, MocpNext, MocpPrevious, @@ -71,6 +72,7 @@ pub enum ActionMap { Quit, RefreshView, RegexMatch, + RemoteMount, Rename, ResetMode, ReverseFlags, @@ -145,6 +147,7 @@ impl ActionMap { ActionMap::MarksNew => EventAction::marks_new(current_tab), ActionMap::MediaInfo => EventAction::mediainfo(current_tab), ActionMap::MocpAddToPlayList => EventAction::mocp_add_to_playlist(current_tab), + ActionMap::MocpClearPlaylist => EventAction::mocp_clear_playlist(), ActionMap::MocpGoToSong => EventAction::mocp_go_to_song(current_tab), ActionMap::MocpNext => EventAction::mocp_next(), ActionMap::MocpPrevious => EventAction::mocp_previous(), @@ -165,6 +168,7 @@ impl ActionMap { ActionMap::Quit => EventAction::quit(current_tab), ActionMap::RefreshView => EventAction::refreshview(status, colors), ActionMap::RegexMatch => EventAction::regex_match(current_tab), + ActionMap::RemoteMount => EventAction::remote_mount(current_tab), ActionMap::Rename => EventAction::rename(current_tab), ActionMap::ResetMode => EventAction::reset_mode(current_tab), ActionMap::ReverseFlags => EventAction::reverse_flags(status), @@ -191,7 +195,7 @@ impl ActionMap { ActionMap::TreeFold => EventAction::tree_fold(current_tab, colors), ActionMap::TreeFoldAll => EventAction::tree_fold_all(current_tab, colors), ActionMap::TreeUnFoldAll => EventAction::tree_unfold_all(current_tab, colors), - ActionMap::Custom(string) => EventAction::custom(status, string.clone()), + ActionMap::Custom(string) => EventAction::custom(status, string), ActionMap::Nothing => Ok(()), } diff --git a/src/bulkrename.rs b/src/bulkrename.rs index aedaaec9..1bbc6787 100644 --- a/src/bulkrename.rs +++ b/src/bulkrename.rs @@ -8,6 +8,7 @@ use std::time::{Duration, SystemTime}; use crate::constant_strings_paths::TMP_FOLDER_PATH; use crate::impl_selectable_content; +use crate::log::write_log_line; use crate::opener::Opener; use crate::status::Status; @@ -115,8 +116,12 @@ impl<'a> Bulkrename<'a> { let mut file = std::fs::File::create(&self.temp_file)?; for path in self.original_filepath.clone().unwrap().iter() { - let Some(os_filename) = path.file_name() else { return Ok(()) }; - let Some(filename) = os_filename.to_str() else {return Ok(()) }; + let Some(os_filename) = path.file_name() else { + return Ok(()); + }; + let Some(filename) = os_filename.to_str() else { + return Ok(()); + }; let b = filename.as_bytes(); file.write_all(b)?; file.write_all(&[b'\n'])?; @@ -169,9 +174,11 @@ impl<'a> Bulkrename<'a> { let new_name = sanitize_filename::sanitize(filename); self.rename_file(path, &new_name)?; counter += 1; - info!(target: "special", "Bulk renamed {path} to {new_name}", path=path.display()) + let log_line = format!("Bulk renamed {path} to {new_name}", path = path.display()); + write_log_line(log_line); } - info!(target: "special", "Bulk renamed {counter} files"); + let log_line = format!("Bulk renamed {counter} files"); + write_log_line(log_line); Ok(()) } @@ -181,24 +188,29 @@ impl<'a> Bulkrename<'a> { let mut new_path = std::path::PathBuf::from(self.parent_dir.unwrap()); if !filename.ends_with('/') { new_path.push(filename); - let Some(parent) = new_path.parent() else { return Ok(()); }; + let Some(parent) = new_path.parent() else { + return Ok(()); + }; info!("Bulk new files. Creating parent: {}", parent.display()); if std::fs::create_dir_all(parent).is_err() { continue; }; info!("creating: {new_path:?}"); std::fs::File::create(&new_path)?; - info!(target:"special", "Bulk created {new_path}", new_path=new_path.display()); + let log_line = format!("Bulk created {new_path}", new_path = new_path.display()); + write_log_line(log_line); counter += 1; } else { new_path.push(filename); info!("Bulk creating dir: {}", new_path.display()); std::fs::create_dir_all(&new_path)?; - info!(target:"special", "Bulk created {new_path}", new_path=new_path.display()); + let log_line = format!("Bulk created {new_path}", new_path = new_path.display()); + write_log_line(log_line); counter += 1; } } - info!(target: "special", "Bulk created {counter} files"); + let log_line = format!("Bulk created {counter} files"); + write_log_line(log_line); Ok(()) } diff --git a/src/cli_info.rs b/src/cli_info.rs index ced4b1a9..aa29b50f 100644 --- a/src/cli_info.rs +++ b/src/cli_info.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; use std::process::{Command, Stdio}; -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use log::info; +use crate::constant_strings_paths::CLI_INFO_COMMANDS; use crate::impl_selectable_content; - +use crate::log::write_log_line; use crate::utils::is_program_in_path; /// Holds the command line commands we can run and display @@ -15,25 +15,21 @@ use crate::utils::is_program_in_path; #[derive(Clone)] pub struct CliInfo { pub content: Vec<&'static str>, - commands: HashMap<&'static str, Vec<&'static str>>, + commands: Vec>, index: usize, } impl Default for CliInfo { fn default() -> Self { let index = 0; - let commands = HashMap::from([ - ("duf", vec!["duf"]), - ("inxi", vec!["inxi", "-FB", "--color"]), - ("neofetch", vec!["neofetch"]), - ("lsusb", vec!["lsusb"]), - ]); - let content: Vec<&'static str> = commands - .keys() - .filter(|s| is_program_in_path(s)) - .copied() + let commands: Vec> = CLI_INFO_COMMANDS + .iter() + .map(|command| command.split(' ').collect::>()) + .filter(|args| is_program_in_path(args[0])) .collect(); + let content: Vec<&str> = commands.iter().map(|args| args[0]).collect(); + Self { content, index, @@ -47,26 +43,31 @@ impl CliInfo { /// Some environement variables are first set to ensure the colored output. /// Long running commands may freeze the display. pub fn execute(&self) -> Result { - let key = self.selected().context("no cli selected")?; - let output = { - let args = self.commands.get(key).context("no arguments for exe")?; - info!("execute. executable: {key}, arguments: {args:?}",); - let child = Command::new(args[0]) - .args(&args[1..]) - .env("CLICOLOR_FORCE", "1") - .env("COLORTERM", "ansi") - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::null()) - .spawn()?; - let output = child.wait_with_output()?; - if output.status.success() { - Ok(String::from_utf8(output.stdout)?) + let args = self.commands[self.index].clone(); + info!("execute. {args:?}"); + let log_line = format!("Executed {args:?}"); + write_log_line(log_line); + let child = Command::new(args[0]) + .args(&args[1..]) + .env("CLICOLOR_FORCE", "1") + .env("COLORTERM", "ansi") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn()?; + let command_output = child.wait_with_output()?; + let text_output = { + if command_output.status.success() { + String::from_utf8(command_output.stdout)? } else { - Err(anyhow!("execute: command didn't finished correctly",)) + format!( + "Command {a} exited with error code {e}", + a = args[0], + e = command_output.status + ) } - }?; - Ok(output) + }; + Ok(text_output) } } diff --git a/src/completion.rs b/src/completion.rs index 15975031..c3fcf75a 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -4,8 +4,7 @@ use anyhow::Result; use strum::IntoEnumIterator; use crate::fileinfo::PathContent; -use crate::mode::Mode; -use crate::tree::ColoredString; +use crate::preview::ColoredTriplet; /// Different kind of completions #[derive(Clone, Default, Copy)] @@ -27,7 +26,6 @@ pub enum InputCompleted { /// showing where the user is in the vec. #[derive(Clone, Default)] pub struct Completion { - kind: InputCompleted, /// Possible completions pub proposals: Vec, /// Which completion is selected by the user @@ -35,14 +33,6 @@ pub struct Completion { } impl Completion { - pub fn set_kind(&mut self, mode: &Mode) { - if let Mode::InputCompleted(completion_kind) = mode { - self.kind = *completion_kind - } else { - self.kind = InputCompleted::Nothing - } - } - /// Is there any completion option ? pub fn is_empty(&self) -> bool { self.proposals.is_empty() @@ -101,21 +91,33 @@ impl Completion { /// Goto completion. /// Looks for the valid path completing what the user typed. pub fn goto(&mut self, input_string: &str, current_path: &str) -> Result<()> { - self.update_from_input(input_string, current_path); + self.goto_update_from_input(input_string, current_path); let (parent, last_name) = split_input_string(input_string); if last_name.is_empty() { return Ok(()); } self.extend_absolute_paths(&parent, &last_name); self.extend_relative_paths(current_path, &last_name); + self.proposals.dedup(); Ok(()) } - fn update_from_input(&mut self, input_string: &str, current_path: &str) { - if let Some(input_path) = self.canonicalize_input(input_string, current_path) { - self.proposals = vec![input_path] + fn goto_update_from_input(&mut self, input_string: &str, current_path: &str) { + self.proposals = vec![]; + if let Some(expanded_input) = self.expand_input(input_string) { + self.proposals.push(expanded_input); + } + if let Some(cannonicalized_input) = self.canonicalize_input(input_string, current_path) { + self.proposals.push(cannonicalized_input); + } + } + + fn expand_input(&mut self, input_string: &str) -> Option { + let expanded_input = shellexpand::tilde(input_string).into_owned(); + if std::path::PathBuf::from(&expanded_input).exists() { + Some(expanded_input) } else { - self.proposals = vec![] + None } } @@ -131,8 +133,12 @@ impl Completion { } fn extend_absolute_paths(&mut self, parent: &str, last_name: &str) { - let Ok(path) = std::fs::canonicalize(parent) else { return }; - let Ok(entries) = fs::read_dir(path) else { return }; + let Ok(path) = std::fs::canonicalize(parent) else { + return; + }; + let Ok(entries) = fs::read_dir(path) else { + return; + }; self.extend(&Self::entries_matching_filename(entries, last_name)) } @@ -162,6 +168,7 @@ impl Completion { Ok(()) } + /// Looks for fm actions completing the one typed by the user. pub fn command(&mut self, input_string: &str) -> Result<()> { let proposals = crate::action_map::ActionMap::iter() .filter(|command| { @@ -208,16 +215,13 @@ impl Completion { pub fn search_from_tree( &mut self, input_string: &str, - content: &[(String, ColoredString)], + content: &[ColoredTriplet], ) -> Result<()> { self.update( content .iter() - .filter(|(_, s)| s.text.contains(input_string)) - .map(|(_, s)| { - let text = s.text.replace("▸ ", ""); - text.replace("▾ ", "") - }) + .filter(|(_, _, s)| s.text.contains(input_string)) + .map(|(_, _, s)| s.text.replace("▸ ", "").replace("▾ ", "")) .collect(), ); @@ -233,7 +237,9 @@ impl Completion { } fn file_match_input(dir_entry: &std::fs::DirEntry, input_string: &str) -> bool { - let Ok(file_type) = dir_entry.file_type() else { return false;}; + let Ok(file_type) = dir_entry.file_type() else { + return false; + }; (file_type.is_file() || file_type.is_symlink()) && filename_startswith(dir_entry, input_string) } diff --git a/src/compress.rs b/src/compress.rs index c41cd510..d1bb6fb7 100644 --- a/src/compress.rs +++ b/src/compress.rs @@ -5,6 +5,7 @@ use std::io::Write; use anyhow::Result; use crate::impl_selectable_content; +use crate::log::write_log_line; use flate2::write::{DeflateEncoder, GzEncoder, ZlibEncoder}; use flate2::Compression; use lzma::LzmaWriter; @@ -56,34 +57,50 @@ impl Default for Compresser { impl Compresser { /// Archive the files with tar and compress them with the selected method. /// The compression method is chosen by the user. - pub fn compress(&self, files: Vec) -> Result<()> { - let Some(selected) = self.selected() else { return Ok(()) }; + /// Archive is created `here` which should be the path of the selected tab. + pub fn compress(&self, files: Vec, here: &std::path::Path) -> Result<()> { + let Some(selected) = self.selected() else { + return Ok(()); + }; match selected { - CompressionMethod::DEFLATE => Self::compress_deflate("archive.tar.gz", files), - CompressionMethod::GZ => Self::compress_gzip("archive.tar.gz", files), - CompressionMethod::ZLIB => Self::compress_zlib("archive.tar.xz", files), - CompressionMethod::ZIP => Self::compress_zip("archive.zip", files), - CompressionMethod::LZMA => Self::compress_lzma("archive.tar.xz", files), + CompressionMethod::DEFLATE => { + Self::defl(Self::archive(here, "archive.tar.gz")?, files)? + } + CompressionMethod::GZ => Self::gzip(Self::archive(here, "archive.tar.gz")?, files)?, + CompressionMethod::ZLIB => Self::zlib(Self::archive(here, "archive.tar.xz")?, files)?, + CompressionMethod::ZIP => Self::zip(Self::archive(here, "archive.zip")?, files)?, + CompressionMethod::LZMA => Self::lzma(Self::archive(here, "archive.tar.xz")?, files)?, } + let log_line = format!("Compressed with {selected}"); + write_log_line(log_line); + Ok(()) } fn make_tar(files: Vec, mut archive: tar::Builder) -> Result<()> where W: Write, { - for file in files.iter() { - if file.is_dir() { - archive.append_dir_all(file, file)?; + for path in files.iter() { + if path.starts_with("..") { + continue; + } + if path.is_dir() { + archive.append_dir_all(path, path)?; } else { - archive.append_path(file)?; + archive.append_path(path)?; } } Ok(()) } - fn compress_gzip(archive_name: &str, files: Vec) -> Result<()> { - let compressed_file = std::fs::File::create(archive_name)?; - let mut encoder = GzEncoder::new(compressed_file, Compression::default()); + fn archive(here: &std::path::Path, archive_name: &str) -> Result { + let mut full_path = here.to_path_buf(); + full_path.push(archive_name); + Ok(std::fs::File::create(full_path)?) + } + + fn gzip(archive: std::fs::File, files: Vec) -> Result<()> { + let mut encoder = GzEncoder::new(archive, Compression::default()); // Create tar archive and compress files Self::make_tar(files, tar::Builder::new(&mut encoder))?; @@ -94,9 +111,8 @@ impl Compresser { Ok(()) } - fn compress_deflate(archive_name: &str, files: Vec) -> Result<()> { - let compressed_file = std::fs::File::create(archive_name)?; - let mut encoder = DeflateEncoder::new(compressed_file, Compression::default()); + fn defl(archive: std::fs::File, files: Vec) -> Result<()> { + let mut encoder = DeflateEncoder::new(archive, Compression::default()); // Create tar archive and compress files Self::make_tar(files, tar::Builder::new(&mut encoder))?; @@ -107,9 +123,8 @@ impl Compresser { Ok(()) } - fn compress_zlib(archive_name: &str, files: Vec) -> Result<()> { - let compressed_file = std::fs::File::create(archive_name)?; - let mut encoder = ZlibEncoder::new(compressed_file, Compression::default()); + fn zlib(archive: std::fs::File, files: Vec) -> Result<()> { + let mut encoder = ZlibEncoder::new(archive, Compression::default()); // Create tar archive and compress files Self::make_tar(files, tar::Builder::new(&mut encoder))?; @@ -120,9 +135,8 @@ impl Compresser { Ok(()) } - fn compress_lzma(archive_name: &str, files: Vec) -> Result<()> { - let compressed_file = std::fs::File::create(archive_name)?; - let mut encoder = LzmaWriter::new_compressor(compressed_file, 6)?; + fn lzma(archive: std::fs::File, files: Vec) -> Result<()> { + let mut encoder = LzmaWriter::new_compressor(archive, 6)?; // Create tar archive and compress files Self::make_tar(files, tar::Builder::new(&mut encoder))?; @@ -133,8 +147,7 @@ impl Compresser { Ok(()) } - fn compress_zip(archive_name: &str, files: Vec) -> Result<()> { - let archive = std::fs::File::create(archive_name).unwrap(); + fn zip(archive: std::fs::File, files: Vec) -> Result<()> { let mut zip = zip::ZipWriter::new(archive); for file in files.iter() { zip.start_file( diff --git a/src/config.rs b/src/config.rs index a758c730..1e4a588d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -30,6 +30,13 @@ impl Settings { } } +macro_rules! update_attribute { + ($self_attr:expr, $yaml:ident, $key:expr) => { + if let Some(attr) = read_yaml_value($yaml, $key) { + $self_attr = attr; + } + }; +} /// Holds every configurable aspect of the application. /// All attributes are hardcoded then updated from optional values /// of the config file. @@ -84,6 +91,10 @@ impl Config { } } +fn read_yaml_value(yaml: &serde_yaml::value::Value, key: &str) -> Option { + yaml[key].as_str().map(|s| s.to_string()) +} + /// Holds configurable colors for every kind of file. /// "Normal" files are displayed with a different color by extension. #[derive(Debug, Clone)] @@ -100,30 +111,21 @@ pub struct Colors { pub socket: String, /// Color for `symlink` files. pub symlink: String, + /// Color for broken `symlink` files. + pub broken: String, /// Colors for normal files, depending of extension pub color_cache: ColorCache, } impl Colors { fn update_from_config(&mut self, yaml: &serde_yaml::value::Value) { - if let Some(directory) = yaml["directory"].as_str().map(|s| s.to_string()) { - self.directory = directory; - } - if let Some(block) = yaml["block"].as_str().map(|s| s.to_string()) { - self.block = block; - } - if let Some(char) = yaml["char"].as_str().map(|s| s.to_string()) { - self.char = char; - } - if let Some(fifo) = yaml["fifo"].as_str().map(|s| s.to_string()) { - self.fifo = fifo; - } - if let Some(socket) = yaml["socket"].as_str().map(|s| s.to_string()) { - self.socket = socket; - } - if let Some(symlink) = yaml["symlink"].as_str().map(|s| s.to_string()) { - self.symlink = symlink; - } + update_attribute!(self.directory, yaml, "directory"); + update_attribute!(self.block, yaml, "block"); + update_attribute!(self.char, yaml, "char"); + update_attribute!(self.fifo, yaml, "fifo"); + update_attribute!(self.socket, yaml, "socket"); + update_attribute!(self.symlink, yaml, "symlink"); + update_attribute!(self.broken, yaml, "broken"); } fn new() -> Self { @@ -134,6 +136,7 @@ impl Colors { fifo: "blue".to_owned(), socket: "cyan".to_owned(), symlink: "magenta".to_owned(), + broken: "white".to_owned(), color_cache: ColorCache::default(), } } @@ -177,7 +180,9 @@ pub fn str_to_tuikit(color: &str) -> Color { pub fn load_config(path: &str) -> Result { let mut config = Config::new()?; let file = File::open(path::Path::new(&shellexpand::tilde(path).to_string()))?; - let Ok(yaml) = serde_yaml::from_reader(file) else { return Ok(config); }; + let Ok(yaml) = serde_yaml::from_reader(file) else { + return Ok(config); + }; let _ = config.update_from_config(&yaml); Ok(config) } diff --git a/src/constant_strings_paths.rs b/src/constant_strings_paths.rs index 854f5094..dfac76c9 100644 --- a/src/constant_strings_paths.rs +++ b/src/constant_strings_paths.rs @@ -1,60 +1,60 @@ /// Configuration folder path -pub static CONFIG_FOLDER: &str = "~/.config/fm"; +pub const CONFIG_FOLDER: &str = "~/.config/fm"; /// Configuration file path -pub static CONFIG_PATH: &str = "~/.config/fm/config.yaml"; +pub const CONFIG_PATH: &str = "~/.config/fm/config.yaml"; /// Filepath of the opener config file -pub static OPENER_PATH: &str = "~/.config/fm/opener.yaml"; +pub const OPENER_PATH: &str = "~/.config/fm/opener.yaml"; /// Filepath of the TUIS configuration file -pub static TUIS_PATH: &str = "~/.config/fm/tuis.yaml"; +pub const TUIS_PATH: &str = "~/.config/fm/tuis.yaml"; /// Filepath of the LOG configuration file -pub static LOG_CONFIG_PATH: &str = "~/.config/fm/logging_config.yaml"; +pub const LOG_CONFIG_PATH: &str = "~/.config/fm/logging_config.yaml"; /// Path to the action log file -pub static ACTION_LOG_PATH: &str = "~/.config/fm/log/action_logger.log"; +pub const ACTION_LOG_PATH: &str = "~/.config/fm/log/action_logger.log"; /// Path to the trash folder files -pub static TRASH_FOLDER_FILES: &str = "~/.local/share/Trash/files"; +pub const TRASH_FOLDER_FILES: &str = "~/.local/share/Trash/files"; /// Path to the trash folder info file -pub static TRASH_FOLDER_INFO: &str = "~/.local/share/Trash/info"; +pub const TRASH_FOLDER_INFO: &str = "~/.local/share/Trash/info"; /// Log file path. Rotating file logs are created in the same directeroy -pub static LOG_PATH: &str = "~/.config/fm/fm{}"; +pub const LOG_PATH: &str = "~/.config/fm/fm{}"; /// File where marks are stored. -pub static MARKS_FILEPATH: &str = "~/.config/fm/marks.cfg"; +pub const MARKS_FILEPATH: &str = "~/.config/fm/marks.cfg"; /// Temporary folder used when bulkrenaming files -pub static TMP_FOLDER_PATH: &str = "/tmp"; +pub const TMP_FOLDER_PATH: &str = "/tmp"; /// Default terminal application used when openening a program in shell or starting a new shell -pub static DEFAULT_TERMINAL_APPLICATION: &str = "st"; +pub const DEFAULT_TERMINAL_APPLICATION: &str = "st"; /// Opener used to play audio files. Does it require a terminal ? -pub static DEFAULT_AUDIO_OPENER: (&str, bool) = ("mocp", true); +pub const DEFAULT_AUDIO_OPENER: (&str, bool) = ("mocp", true); /// Program used to to display images. Does it require a terminal ? -pub static DEFAULT_IMAGE_OPENER: (&str, bool) = ("viewnior", false); +pub const DEFAULT_IMAGE_OPENER: (&str, bool) = ("viewnior", false); /// Program used to open "office" documents (word, libreoffice etc). Does it require a terminal ? -pub static DEFAULT_OFFICE_OPENER: (&str, bool) = ("libreoffice", false); +pub const DEFAULT_OFFICE_OPENER: (&str, bool) = ("libreoffice", false); /// Program used to open readable documents (pdf, ebooks). Does it require a terminal ? -pub static DEFAULT_READABLE_OPENER: (&str, bool) = ("zathura", false); +pub const DEFAULT_READABLE_OPENER: (&str, bool) = ("zathura", false); /// Program used to open text files. Does it require a terminal ? -pub static DEFAULT_TEXT_OPENER: (&str, bool) = ("nvim", true); +pub const DEFAULT_TEXT_OPENER: (&str, bool) = ("nvim", true); /// Program used to open unknown files. Does it require a terminal ? -pub static DEFAULT_OPENER: (&str, bool) = ("xdg-open", false); +pub const DEFAULT_OPENER: (&str, bool) = ("xdg-open", false); /// Program used to open vectorial images. Does it require a terminal ? -pub static DEFAULT_VECTORIAL_OPENER: (&str, bool) = ("inkscape", false); +pub const DEFAULT_VECTORIAL_OPENER: (&str, bool) = ("inkscape", false); /// Program used to open videos. Does it require a terminal ? -pub static DEFAULT_VIDEO_OPENER: (&str, bool) = ("mpv", false); +pub const DEFAULT_VIDEO_OPENER: (&str, bool) = ("mpv", false); /// Default program used to drag and drop files -pub static DEFAULT_DRAGNDROP: &str = "dragon-drop"; +pub const DEFAULT_DRAGNDROP: &str = "dragon-drop"; /// Array of text representation of a file permissions. /// The index of each string gives a correct representation. -pub static PERMISSIONS_STR: [&str; 8] = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; +pub const PERMISSIONS_STR: [&str; 8] = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; /// Description of the application. -pub static HELP_FIRST_SENTENCE: &str = "fm: a dired / ranger like file manager. "; +pub const HELP_FIRST_SENTENCE: &str = " fm: a dired / ranger like file manager. "; /// Description of the content below, aka the help itself. -pub static HELP_SECOND_SENTENCE: &str = "Keybindings"; +pub const HELP_SECOND_SENTENCE: &str = " Keybindings "; /// Description of the content below, aka the action log file -pub static LOG_FIRST_SENTENCE: &str = "Logs: "; +pub const LOG_FIRST_SENTENCE: &str = " Logs: "; /// Description of the content below, aka what is logged there. -pub static LOG_SECOND_SENTENCE: &str = "Last actions affecting the file tree"; +pub const LOG_SECOND_SENTENCE: &str = " Last actions affecting the file tree"; /// Video thumbnails -pub static THUMBNAIL_PATH: &str = "/tmp/thumbnail.png"; +pub const THUMBNAIL_PATH: &str = "/tmp/thumbnail.png"; /// Array of hardcoded shortcuts with standard *nix paths. -pub static HARDCODED_SHORTCUTS: [&str; 9] = [ +pub const HARDCODED_SHORTCUTS: [&str; 9] = [ "/", "/dev", "/etc", @@ -65,12 +65,14 @@ pub static HARDCODED_SHORTCUTS: [&str; 9] = [ "/usr", "/var", ]; -pub static BAT_EXECUTABLE: &str = "bat {} --color=always"; -pub static CAT_EXECUTABLE: &str = "cat {}"; -pub static RG_EXECUTABLE: &str = "rg --line-number \"{}\""; -pub static GREP_EXECUTABLE: &str = "grep -rI --line-number \"{}\""; +pub const BAT_EXECUTABLE: &str = "bat {} --color=always"; +pub const CAT_EXECUTABLE: &str = "cat {}"; +pub const RG_EXECUTABLE: &str = "rg --line-number \"{}\""; +pub const GREP_EXECUTABLE: &str = "grep -rI --line-number \"{}\""; +pub const SSHFS_EXECUTABLE: &str = "sshfs"; +pub const NOTIFY_EXECUTABLE: &str = "notity-send"; /// Sort presentation for the second window -pub static SORT_LINES: [&str; 7] = [ +pub const SORT_LINES: [&str; 7] = [ "k: by kind (default)", "n: by name", "m: by modification time", @@ -79,8 +81,14 @@ pub static SORT_LINES: [&str; 7] = [ "", "r: reverse current sort", ]; +pub const REMOTE_LINES: [&str; 4] = [ + "Mount a directory with sshfs", + "Type the arguments as below, separated by a space", + "", + "username hostname remote_path", +]; /// Chmod presentation for the second window -pub static CHMOD_LINES: [&str; 5] = [ +pub const CHMOD_LINES: [&str; 5] = [ "Type an octal mode like 754.", "", "4: read", @@ -88,7 +96,7 @@ pub static CHMOD_LINES: [&str; 5] = [ "1: execute", ]; /// Filter presentation for the second window -pub static FILTER_LINES: [&str; 6] = [ +pub const FILTER_LINES: [&str; 6] = [ "Type the initial of the filter and an expression if needed", "", "n {name}: by name", @@ -97,10 +105,10 @@ pub static FILTER_LINES: [&str; 6] = [ "a: reset", ]; /// Password input presentation for the second window -pub static PASSWORD_LINES: [&str; 1] = +pub const PASSWORD_LINES: [&str; 1] = ["Type your password. It will be forgotten immediatly after use."]; /// Shell presentation for the second window -pub static SHELL_LINES: [&str; 11] = [ +pub const SHELL_LINES: [&str; 11] = [ "Type a shell command", "", "`sudo` commands are supported.", @@ -114,33 +122,65 @@ pub static SHELL_LINES: [&str; 11] = [ "%s: selected filepath", ]; /// Nvim address setter presentation for second window -pub static NVIM_ADDRESS_LINES: [&str; 4] = [ +pub const NVIM_ADDRESS_LINES: [&str; 4] = [ "Type the Neovim RPC address.", "", "You can get it from Neovim with :", "`:echo v:servername`", ]; /// Regex matcher presentation for second window -pub static REGEX_LINES: [&str; 3] = [ +pub const REGEX_LINES: [&str; 3] = [ "Type a regular expression", "", "Flag every file in current directory matching the typed regex", ]; /// Newdir presentation for second window -pub static NEWDIR_LINES: [&str; 3] = [ +pub const NEWDIR_LINES: [&str; 3] = [ "mkdir a new directory", "", "Nothing is done if the directory already exists", ]; /// New file presentation for second window -pub static NEWFILE_LINES: [&str; 3] = [ +pub const NEWFILE_LINES: [&str; 3] = [ "touch a new file", "", "Nothing is done if the file already exists", ]; /// Rename presentation for second window -pub static RENAME_LINES: [&str; 3] = [ +pub const RENAME_LINES: [&str; 3] = [ "rename the selected file", "", "Nothing is done if the file already exists", ]; +/// Executable commands whose output is a text to be displayed in terminal +pub const CLI_INFO_COMMANDS: [&str; 4] = ["duf", "inxi -v 2 --color", "neofetch", "lsusb"]; +/// Wallpaper executable +pub const NITROGEN: &str = "nitrogen"; +/// Mediainfo (used to preview media files) executable +pub const MEDIAINFO: &str = "mediainfo"; +/// ueberzug (used to preview images, videos & fonts) +pub const UEBERZUG: &str = "ueberzug"; +/// fontimage (used to preview fonts) +pub const FONTIMAGE: &str = "fontimage"; +/// ffmpeg (used to preview video thumbnail) +pub const FFMPEG: &str = "ffmpeg"; +/// rsvg-convert (used to preview svg files) +pub const RSVG_CONVERT: &str = "rsvg-convert"; +/// jupyter. used to preview notebooks (.ipynb) +pub const JUPYTER: &str = "jupyter"; +/// pandoc. used to preview .doc & .odb documents +pub const PANDOC: &str = "pandoc"; +/// diff. used to preview diff between files +pub const DIFF: &str = "diff"; +/// isoinfo. used to preview iso file content +pub const ISOINFO: &str = "isoinfo"; +/// socket file explorer +pub const SS: &str = "ss"; +/// lsblk is used to get mountpoints, info about encrypted drives +pub const LSBLK: &str = "lsblk"; +/// cryptsetup is used to mount encrypted drives +pub const CRYPTSETUP: &str = "cryptsetup"; +/// used to get information about fifo files +pub const LSOF: &str = "lsof"; +/// neovim executable +pub const NVIM: &str = "nvim"; diff --git a/src/content_window.rs b/src/content_window.rs index 6c0ac1c9..a734387f 100644 --- a/src/content_window.rs +++ b/src/content_window.rs @@ -1,6 +1,5 @@ use std::cmp::{max, min}; -pub const RESERVED_ROWS: usize = 3; /// Holds the information about the displayed window of lines. /// When there's too much lines to display in one screen, we can scroll /// and this struct is responsible for that. @@ -8,37 +7,59 @@ pub const RESERVED_ROWS: usize = 3; /// methods. #[derive(Debug, Clone)] pub struct ContentWindow { - /// The index of the first displayed file. + /// The index of the first displayed element. pub top: usize, - /// The index of the last displayed file. + /// The index of the last displayed element + 1. pub bottom: usize, - /// The number of displayble file in the current folder. + /// The number of displayble elements. pub len: usize, /// The height of the terminal. pub height: usize, } impl ContentWindow { + /// The padding around the last displayed filename + const WINDOW_PADDING: usize = 4; + /// The space of the top element + pub const WINDOW_MARGIN_TOP: usize = 2; + /// How many rows are reserved for the header ? + pub const HEADER_ROWS: usize = 3; + /// How many rows are reserved for the footer ? + const FOOTER_ROWS: usize = 1; + /// Footer and bottom padding + const BOTTOM_ROWS: usize = 2; + + /// How many rows could be displayed with given height ? + /// It's not the number of rows displayed since the content may + /// not be long enough to fill the window. + fn nb_displayed_rows(height: usize) -> usize { + height - Self::HEADER_ROWS - Self::FOOTER_ROWS + } + + /// Default value for the bottom index. + /// minimum of terminal height minus reserved rows and the length of the content. + fn default_bottom(len: usize, height: usize) -> usize { + min(height - Self::BOTTOM_ROWS, len) + } + /// Returns a new `ContentWindow` instance with values depending of - /// number of files and height of the terminal screen. - pub fn new(len: usize, height: usize) -> Self { + /// number of displayable elements and height of the terminal screen. + pub fn new(len: usize, terminal_height: usize) -> Self { + let height = Self::nb_displayed_rows(terminal_height); + let top = 0; + let bottom = Self::default_bottom(len, height); ContentWindow { - top: 0, - bottom: min(len, height - RESERVED_ROWS), + top, + bottom, len, - height: height - RESERVED_ROWS, + height, } } - /// The padding around the last displayed filename - const WINDOW_PADDING: usize = 4; - /// The space of the top element - pub const WINDOW_MARGIN_TOP: usize = 2; - /// How many rows are reserved at bottom - /// Set the height of file window. - pub fn set_height(&mut self, height: usize) { - self.height = height - RESERVED_ROWS; + pub fn set_height(&mut self, terminal_height: usize) { + self.height = Self::nb_displayed_rows(terminal_height); + self.bottom = Self::default_bottom(self.len, self.height); } /// Move the window one line up if possible. @@ -61,23 +82,34 @@ impl ContentWindow { self.top += 1; self.bottom += 1; } - self.scroll_to(index) } - /// Reset the window to the first files of the current directory. - pub fn reset(&mut self, len: usize) { - self.len = len; - self.top = 0; - self.bottom = min(len, self.height); - } - /// Scroll the window to this index if possible. /// Does nothing if the index can't be reached. pub fn scroll_to(&mut self, index: usize) { - if index < self.top || index > self.bottom { + if self.len < self.height { + return; + } + if self.is_index_outside_window(index) { self.top = max(index, Self::WINDOW_PADDING) - Self::WINDOW_PADDING; - self.bottom = self.top + min(self.len, self.height - 3); + self.bottom = (self.top + Self::default_bottom(self.height, self.len)) + .checked_sub(Self::BOTTOM_ROWS) + .unwrap_or(2); } } + + /// Reset the window to the first item of the content. + pub fn reset(&mut self, len: usize) { + self.len = len; + self.top = 0; + self.bottom = Self::default_bottom(self.len, self.height); + } + + /// True iff the index is outside the displayed window or + /// too close from the border. + /// User shouldn't be able to reach the last elements + fn is_index_outside_window(&self, index: usize) -> bool { + index < self.top || index >= self.bottom + } } diff --git a/src/copy_move.rs b/src/copy_move.rs index 2da977b8..dd49f952 100644 --- a/src/copy_move.rs +++ b/src/copy_move.rs @@ -9,57 +9,34 @@ use indicatif::{InMemoryTerm, ProgressBar, ProgressDrawTarget, ProgressState, Pr use log::info; use tuikit::prelude::{Attr, Color, Effect, Event, Term}; +use crate::constant_strings_paths::NOTIFY_EXECUTABLE; use crate::fileinfo::human_size; +use crate::log::write_log_line; use crate::opener::execute_in_child; +use crate::utils::is_program_in_path; -fn setup( - action: String, - height: usize, - width: usize, -) -> Result<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> { - let in_mem = InMemoryTerm::new(height as u16, width as u16); - let pb = ProgressBar::with_draw_target( - Some(100), - ProgressDrawTarget::term_like(Box::new(in_mem.clone())), - ); - pb.set_style( - ProgressStyle::with_template( - "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})", - ) - .unwrap() - .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { - write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() - }) - .with_key("action", move |_: &ProgressState, w: &mut dyn Write| { - write!(w, "{}", &action).unwrap() - }) - .progress_chars("#>-"), - ); - let options = fs_extra::dir::CopyOptions::new(); - Ok((in_mem, pb, options)) -} - +/// Display the updated progress bar on the terminal. fn handle_progress_display( in_mem: &InMemoryTerm, pb: &ProgressBar, term: &Arc, process_info: fs_extra::TransitProcess, ) -> fs_extra::dir::TransitProcessResult { - pb.set_position(100 * process_info.copied_bytes / process_info.total_bytes); - let _ = term.print_with_attr( - 1, - 0, - &in_mem.to_owned().contents(), - Attr { - fg: Color::CYAN, - bg: Color::default(), - effect: Effect::REVERSE | Effect::BOLD, - }, - ); + pb.set_position(progress_bar_position(&process_info)); + let _ = term.print_with_attr(1, 1, &in_mem.contents(), CopyMove::attr()); let _ = term.present(); fs_extra::dir::TransitProcessResult::ContinueOrAbort } +/// Position of the progress bar. +/// We have to handle properly 0 bytes to avoid division by zero. +fn progress_bar_position(process_info: &fs_extra::TransitProcess) -> u64 { + if process_info.total_bytes == 0 { + return 0; + } + 100 * process_info.copied_bytes / process_info.total_bytes +} + /// Different kind of movement of files : copying or moving. #[derive(Debug)] pub enum CopyMove { @@ -68,19 +45,80 @@ pub enum CopyMove { } impl CopyMove { + fn attr() -> Attr { + Attr { + fg: Color::CYAN, + bg: Color::Default, + effect: Effect::REVERSE | Effect::BOLD, + } + } + fn verb(&self) -> &str { - match *self { + match self { CopyMove::Copy => "copy", CopyMove::Move => "move", } } fn preterit(&self) -> &str { - match *self { + match self { CopyMove::Copy => "copied", CopyMove::Move => "moved", } } + + fn copier( + &self, + ) -> for<'a, 'b> fn( + &'a [P], + Q, + &'b fs_extra::dir::CopyOptions, + F, + ) -> Result + where + P: AsRef, + Q: AsRef, + F: FnMut(fs_extra::TransitProcess) -> fs_extra::dir::TransitProcessResult, + { + match self { + CopyMove::Copy => fs_extra::copy_items_with_progress, + CopyMove::Move => fs_extra::move_items_with_progress, + } + } + + fn log_and_notify(&self, hs_bytes: String) { + let message = format!("{preterit} {hs_bytes} bytes", preterit = self.preterit()); + let _ = notify(&message); + info!("{message}"); + write_log_line(message); + } + + fn setup_progress_bar( + &self, + size: (usize, usize), + ) -> Result<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> { + let (height, width) = size; + let in_mem = InMemoryTerm::new(height as u16, width as u16); + let pb = ProgressBar::with_draw_target( + Some(100), + ProgressDrawTarget::term_like(Box::new(in_mem.clone())), + ); + let action = self.verb().to_owned(); + pb.set_style( + ProgressStyle::with_template( + "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})", + )? + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .with_key("action", move |_: &ProgressState, w: &mut dyn Write| { + write!(w, "{}", &action).unwrap() + }) + .progress_chars("#>-"), + ); + let options = fs_extra::dir::CopyOptions::new(); + Ok((in_mem, pb, options)) + } } /// Will copy or move a bunch of files to `dest`. @@ -94,49 +132,33 @@ pub fn copy_move( term: Arc, ) -> Result<()> { let c_term = term.clone(); - let (height, width) = term.term_size()?; - let (in_mem, pb, options) = setup(copy_or_move.verb().to_owned(), height, width)?; + let (in_mem, pb, options) = copy_or_move.setup_progress_bar(term.term_size()?)?; let handle_progress = move |process_info: fs_extra::TransitProcess| { handle_progress_display(&in_mem, &pb, &term, process_info) }; let dest = dest.to_owned(); let _ = thread::spawn(move || { - let copier_mover = match copy_or_move { - CopyMove::Copy => fs_extra::copy_items_with_progress, - CopyMove::Move => fs_extra::move_items_with_progress, - }; - let transfered_bytes = match copier_mover(&sources, &dest, &options, handle_progress) { - Ok(transfered_bytes) => transfered_bytes, - Err(e) => { - info!("copy move couldn't copy: {:?}", e); - 0 - } - }; + let transfered_bytes = + match copy_or_move.copier()(&sources, &dest, &options, handle_progress) { + Ok(transfered_bytes) => transfered_bytes, + Err(e) => { + info!("copy move couldn't copy: {e:?}"); + 0 + } + }; let _ = c_term.send_event(Event::User(())); - let _ = notify(&format!( - "fm: {} finished {}B {}", - copy_or_move.verb(), - human_size(transfered_bytes), - copy_or_move.preterit() - )); - info!( - "{} finished {}B", - copy_or_move.verb(), - human_size(transfered_bytes) - ); - info!(target: "special", - "{} finished {}B", - copy_or_move.verb(), - human_size(transfered_bytes) - ) + + copy_or_move.log_and_notify(human_size(transfered_bytes)); }); Ok(()) } /// Send a notification to the desktop. -/// Requires "notify-send" to be installed. +/// Does nothing if "notify-send" isn't installed. fn notify(text: &str) -> Result<()> { - execute_in_child("notify-send", &[text])?; + if is_program_in_path(NOTIFY_EXECUTABLE) { + execute_in_child(NOTIFY_EXECUTABLE, &[text])?; + } Ok(()) } diff --git a/src/cryptsetup.rs b/src/cryptsetup.rs index e28adf94..f72b352a 100644 --- a/src/cryptsetup.rs +++ b/src/cryptsetup.rs @@ -4,13 +4,14 @@ use anyhow::{anyhow, Context, Result}; use log::info; use sysinfo::{DiskExt, System, SystemExt}; +use crate::constant_strings_paths::{CRYPTSETUP, LSBLK}; use crate::impl_selectable_content; use crate::mount_help::MountHelper; use crate::password::{ drop_sudo_privileges, execute_sudo_command, execute_sudo_command_with_password, reset_sudo_faillock, PasswordHolder, PasswordKind, }; -use crate::utils::current_username; +use crate::utils::{current_username, is_program_in_path}; /// Possible actions on encrypted drives #[derive(Debug, Clone, Copy)] @@ -26,7 +27,7 @@ pub enum BlockDeviceAction { /// ``` /// as a String. fn get_devices() -> Result { - let output = Command::new("lsblk") + let output = Command::new(LSBLK) .args(&vec!["-l", "-o", "FSTYPE,PATH,UUID,FSVER,MOUNTPOINT"]) .stdin(Stdio::null()) .stderr(Stdio::null()) @@ -44,6 +45,12 @@ fn filter_crypto_devices_lines(output: String, key: &str) -> Vec { .collect() } +/// True iff `lsblk` and `cryptsetup` are in path. +/// Nothing here can be done without those programs. +pub fn lsblk_and_cryptsetup_installed() -> bool { + is_program_in_path(LSBLK) && is_program_in_path(CRYPTSETUP) +} + /// Represent an encrypted device. /// Those attributes comes from cryptsetup. #[derive(Debug, Default, Clone)] @@ -88,7 +95,7 @@ impl CryptoDevice { fn format_luksopen_parameters(&self) -> [String; 4] { [ - "cryptsetup".to_owned(), + CRYPTSETUP.to_owned(), "open".to_owned(), self.path.clone(), self.uuid.clone(), @@ -96,7 +103,7 @@ impl CryptoDevice { } fn format_luksclose_parameters(&self) -> [String; 3] { [ - "cryptsetup".to_owned(), + CRYPTSETUP.to_owned(), "luksClose".to_owned(), self.device_name .clone() @@ -117,7 +124,7 @@ impl CryptoDevice { } fn set_device_name(&mut self) -> Result<()> { - let child = Command::new("lsblk") + let child = Command::new(LSBLK) .arg("-l") .arg("-n") .arg(self.path.clone()) diff --git a/src/event_dispatch.rs b/src/event_dispatch.rs index f608fcb7..844924fe 100644 --- a/src/event_dispatch.rs +++ b/src/event_dispatch.rs @@ -6,7 +6,6 @@ use crate::event_exec::{EventAction, LeaveMode}; use crate::keybindings::Bindings; use crate::mode::{InputSimple, MarkAction, Mode, Navigate}; use crate::status::Status; -use crate::tab::Tab; /// Struct which mutates `tabs.selected().. /// Holds a mapping which can't be static since it's read from a config file. @@ -27,7 +26,13 @@ impl EventDispatcher { /// Only non keyboard events are dealt here directly. /// Keyboard events are configurable and are sent to specific functions /// which needs to know those keybindings. - pub fn dispatch(&self, status: &mut Status, ev: Event, colors: &Colors) -> Result<()> { + pub fn dispatch( + &self, + status: &mut Status, + ev: Event, + colors: &Colors, + current_height: usize, + ) -> Result<()> { match ev { Event::Key(Key::WheelUp(_, col, _)) => { status.select_pane(col)?; @@ -38,18 +43,18 @@ impl EventDispatcher { EventAction::move_down(status, colors)?; } Event::Key(Key::SingleClick(MouseButton::Left, row, col)) => { - status.select_pane(col)?; - status.selected().select_row(row, colors)?; + status.click(row, col, current_height, colors)?; } - Event::Key(Key::SingleClick(MouseButton::Right, row, col)) - | Event::Key(Key::DoubleClick(MouseButton::Left, row, col)) => { - status.select_pane(col)?; - status.selected().select_row(row, colors)?; + Event::Key( + Key::SingleClick(MouseButton::Right, row, col) + | Key::DoubleClick(MouseButton::Left, row, col), + ) => { + status.click(row, col, current_height, colors)?; LeaveMode::right_click(status, colors)?; } Event::User(_) => status.refresh_status(colors)?, - Event::Resize { width, height } => status.resize(width, height, colors)?, - Event::Key(Key::Char(c)) => self.char(status, Key::Char(c), colors)?, + Event::Resize { width, height } => status.resize(width, height)?, + Event::Key(Key::Char(c)) => self.char(status, c, colors)?, Event::Key(key) => self.key_matcher(status, key, colors)?, _ => (), }; @@ -67,60 +72,37 @@ impl EventDispatcher { } } - fn char(&self, status: &mut Status, key_char: Key, colors: &Colors) -> Result<()> { - match key_char { - Key::Char(c) => match status.selected_non_mut().mode { - Mode::InputSimple(InputSimple::Sort) => status.selected().sort(c, colors), - Mode::InputSimple(InputSimple::RegexMatch) => { - { - let tab: &mut Tab = status.selected(); - tab.input.insert(c); - }; - status.select_from_regex()?; - Ok(()) - } - Mode::InputSimple(_) => { - { - let tab: &mut Tab = status.selected(); - tab.input.insert(c); - }; - Ok(()) - } - Mode::InputCompleted(_) => status.selected().text_insert_and_complete(c), - Mode::Normal | Mode::Tree => match self.binds.get(&key_char) { - Some(char) => char.matcher(status, colors), - None => Ok(()), - }, - Mode::NeedConfirmation(confirmed_action) => { - if c == 'y' { - let _ = status.confirm_action(confirmed_action, colors); - } - status.selected().reset_mode(); - Ok(()) - } - Mode::Navigate(Navigate::Trash) if c == 'x' => status.trash.remove(), - Mode::Navigate(Navigate::EncryptedDrive) if c == 'm' => { - status.mount_encrypted_drive() - } - Mode::Navigate(Navigate::EncryptedDrive) if c == 'g' => { - status.move_to_encrypted_drive() - } - Mode::Navigate(Navigate::EncryptedDrive) if c == 'u' => { - status.umount_encrypted_drive() - } - Mode::Navigate(Navigate::Marks(MarkAction::Jump)) => { - status.marks_jump_char(c, colors) - } - Mode::Navigate(Navigate::Marks(MarkAction::New)) => status.marks_new(c, colors), - Mode::Preview | Mode::Navigate(_) => { - status.selected().set_mode(Mode::Normal); - { - let tab: &mut Tab = status.selected(); - tab.refresh_view() - } - } + fn char(&self, status: &mut Status, c: char, colors: &Colors) -> Result<()> { + let tab = status.selected(); + match tab.mode { + Mode::InputSimple(InputSimple::Sort) => tab.sort(c, colors), + Mode::InputSimple(InputSimple::RegexMatch) => { + tab.input.insert(c); + status.select_from_regex()?; + Ok(()) + } + Mode::InputSimple(_) => { + tab.input.insert(c); + Ok(()) + } + Mode::InputCompleted(_) => tab.text_insert_and_complete(c), + Mode::Normal | Mode::Tree => match self.binds.get(&Key::Char(c)) { + Some(action) => action.matcher(status, colors), + None => Ok(()), }, - _ => Ok(()), + Mode::NeedConfirmation(confirmed_action) => status.confirm(c, confirmed_action, colors), + Mode::Navigate(Navigate::Trash) if c == 'x' => status.trash.remove(), + Mode::Navigate(Navigate::EncryptedDrive) if c == 'm' => status.mount_encrypted_drive(), + Mode::Navigate(Navigate::EncryptedDrive) if c == 'g' => status.go_to_encrypted_drive(), + Mode::Navigate(Navigate::EncryptedDrive) if c == 'u' => status.umount_encrypted_drive(), + Mode::Navigate(Navigate::Marks(MarkAction::Jump)) => status.marks_jump_char(c, colors), + Mode::Navigate(Navigate::Marks(MarkAction::New)) => status.marks_new(c, colors), + Mode::Preview | Mode::Navigate(_) => { + if tab.reset_mode() { + tab.refresh_view()?; + } + Ok(()) + } } } } diff --git a/src/event_exec.rs b/src/event_exec.rs index 63272638..2b1b113f 100644 --- a/src/event_exec.rs +++ b/src/event_exec.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::fmt::Display; use std::fs; use std::path; use std::str::FromStr; @@ -11,23 +12,31 @@ use which::which; use crate::action_map::ActionMap; use crate::completion::InputCompleted; use crate::config::Colors; +use crate::constant_strings_paths::DIFF; +use crate::constant_strings_paths::MEDIAINFO; +use crate::constant_strings_paths::NITROGEN; +use crate::constant_strings_paths::SSHFS_EXECUTABLE; use crate::constant_strings_paths::{CONFIG_PATH, DEFAULT_DRAGNDROP}; -use crate::cryptsetup::BlockDeviceAction; +use crate::cryptsetup::{lsblk_and_cryptsetup_installed, BlockDeviceAction}; use crate::fileinfo::FileKind; use crate::filter::FilterKind; use crate::log::read_log; +use crate::log::write_log_line; +use crate::mocp::is_mocp_installed; use crate::mocp::Mocp; use crate::mode::{InputSimple, MarkAction, Mode, Navigate, NeedConfirmation}; use crate::opener::{ - execute_and_capture_output_without_check, execute_in_child, + execute_and_capture_output, execute_and_capture_output_without_check, execute_in_child, execute_in_child_without_output_with_path, InternalVariant, }; use crate::password::{PasswordKind, PasswordUsage}; +use crate::preview::is_ext_image; use crate::preview::Preview; use crate::selectable_content::SelectableContent; use crate::shell_parser::ShellCommandParser; use crate::status::Status; use crate::tab::Tab; +use crate::utils::is_program_in_path; use crate::utils::{ args_is_empty, disk_used_by_path, filename_from_path, is_sudo_command, open_in_current_neovim, opt_mount_point, string_to_path, @@ -73,7 +82,9 @@ impl EventAction { match tab.mode { Mode::Normal => { - let Some(file) = tab.path_content.selected() else { return Ok(()) }; + let Some(file) = tab.path_content.selected() else { + return Ok(()); + }; let path = file.path.clone(); status.toggle_flag_on_path(&path); status.selected().down_one_row(); @@ -100,7 +111,7 @@ impl EventAction { .flagged .push(status.tabs[status.index].selected().unwrap().path.clone()); }; - status.reset_tabs_view() + Ok(()) } /// Enter JUMP mode, allowing to jump to any flagged file. @@ -141,7 +152,12 @@ impl EventAction { .directory_of_selected()? .join(filename); std::os::unix::fs::symlink(original_file, &link)?; - info!(target: "special", "Symlink {link} links to {original_file}", original_file=original_file.display(), link=link.display()); + let log_line = format!( + "Symlink {link} links to {original_file}", + original_file = original_file.display(), + link = link.display() + ); + write_log_line(log_line); } status.clear_flags_and_reset_view() } @@ -156,8 +172,11 @@ impl EventAction { /// Leave current mode to normal mode. /// Reset the inputs and completion, reset the window, exit the preview. pub fn reset_mode(tab: &mut Tab) -> Result<()> { - tab.reset_mode(); - tab.refresh_view() + if tab.reset_mode() { + tab.refresh_view() + } else { + tab.refresh_params() + } } /// Enter a copy paste mode. /// A confirmation is asked before copying all flagged files to @@ -215,7 +234,9 @@ impl EventAction { return Ok(()); } let unmutable_tab = status.selected_non_mut(); - let Some(file_info) = unmutable_tab.selected() else { return Ok(()) }; + let Some(file_info) = unmutable_tab.selected() else { + return Ok(()); + }; match file_info.file_kind { FileKind::NormalFile => { let preview = Preview::new( @@ -411,7 +432,7 @@ impl EventAction { /// Basic folders (/, /dev... $HOME) and mount points (even impossible to /// visit ones) are proposed. pub fn shortcut(tab: &mut Tab) -> Result<()> { - std::env::set_current_dir(tab.current_path())?; + std::env::set_current_dir(tab.current_directory_path())?; tab.shortcut.update_git_root(); tab.set_mode(Mode::Navigate(Navigate::Shortcut)); Ok(()) @@ -427,8 +448,12 @@ impl EventAction { }; let nvim_server = status.nvim_server.clone(); let tab = status.selected(); - let Some(fileinfo) = tab.selected() else { return Ok(()) }; - let Some(path_str) = fileinfo.path.to_str() else { return Ok(()) }; + let Some(fileinfo) = tab.selected() else { + return Ok(()); + }; + let Some(path_str) = fileinfo.path.to_str() else { + return Ok(()); + }; open_in_current_neovim(path_str, &nvim_server); Ok(()) @@ -486,8 +511,13 @@ impl EventAction { /// Executes a `dragon-drop` command on the selected file. /// It obviously requires the `dragon-drop` command to be installed. pub fn drag_n_drop(status: &mut Status) -> Result<()> { - let tab = status.selected_non_mut(); - let Some(file) = tab.selected() else { return Ok(()) }; + if !is_program_in_path(DEFAULT_DRAGNDROP) { + write_log_line(format!("{DEFAULT_DRAGNDROP} must be installed.")); + return Ok(()); + } + let Some(file) = status.selected_non_mut().selected() else { + return Ok(()); + }; let path_str = file .path .to_str() @@ -501,7 +531,9 @@ impl EventAction { match tab.mode { Mode::Tree => (), _ => { - let Some(searched) = tab.searched.clone() else { return Ok(()) }; + let Some(searched) = tab.searched.clone() else { + return Ok(()); + }; let next_index = (tab.path_content.index + 1) % tab.path_content.content.len(); tab.search_from(&searched, next_index); } @@ -680,6 +712,7 @@ impl EventAction { must_reset_mode = false; LeaveMode::password(status, kind, colors, dest, action)? } + Mode::InputSimple(InputSimple::Remote) => LeaveMode::remote(status.selected())?, Mode::Navigate(Navigate::Jump) => LeaveMode::jump(status)?, Mode::Navigate(Navigate::History) => LeaveMode::history(status.selected())?, Mode::Navigate(Navigate::Shortcut) => LeaveMode::shortcut(status.selected())?, @@ -782,8 +815,14 @@ impl EventAction { /// Display mediainfo details of an image pub fn mediainfo(tab: &mut Tab) -> Result<()> { + if !is_program_in_path(MEDIAINFO) { + write_log_line(format!("{} isn't installed", MEDIAINFO)); + return Ok(()); + } if let Mode::Normal | Mode::Tree = tab.mode { - let Some(file_info) = tab.selected() else { return Ok(())}; + let Some(file_info) = tab.selected() else { + return Ok(()); + }; info!("selected {:?}", file_info); tab.preview = Preview::mediainfo(&file_info.path)?; tab.window.reset(tab.preview.len()); @@ -794,12 +833,20 @@ impl EventAction { /// Display a diff between the first 2 flagged files or dir. pub fn diff(status: &mut Status) -> Result<()> { + if !is_program_in_path(DIFF) { + write_log_line(format!("{DIFF} isn't installed")); + return Ok(()); + } if status.flagged.len() < 2 { return Ok(()); }; if let Mode::Normal | Mode::Tree = status.selected_non_mut().mode { - let first_path = &status.flagged.content[0].to_str().unwrap(); - let second_path = &status.flagged.content[1].to_str().unwrap(); + let first_path = &status.flagged.content[0] + .to_str() + .context("Couldn't parse filename")?; + let second_path = &status.flagged.content[1] + .to_str() + .context("Couldn't parse filename")?; status.selected().preview = Preview::diff(first_path, second_path)?; let tab = status.selected(); tab.window.reset(tab.preview.len()); @@ -912,6 +959,10 @@ impl EventAction { /// Enter the encrypted device menu, allowing the user to mount/umount /// a luks encrypted device. pub fn encrypted_drive(status: &mut Status) -> Result<()> { + if !lsblk_and_cryptsetup_installed() { + write_log_line("lsblk and cryptsetup must be installed.".to_owned()); + return Ok(()); + } if status.encrypted_devices.is_empty() { status.encrypted_devices.update()?; } @@ -958,18 +1009,46 @@ impl EventAction { /// Set the current selected file as wallpaper with `nitrogen`. /// Requires `nitrogen` to be installed. pub fn set_wallpaper(tab: &Tab) -> Result<()> { - let Some(path_str) = tab.path_content.selected_path_string() else { return Ok(()); }; - let _ = execute_in_child("nitrogen", &["--set-zoom-fill", "--save", &path_str]); + if !is_program_in_path(NITROGEN) { + write_log_line("nitrogen must be installed".to_owned()); + return Ok(()); + } + let Some(fileinfo) = tab.path_content.selected() else { + return Ok(()); + }; + if !is_ext_image(&fileinfo.extension) { + return Ok(()); + } + let Some(path_str) = tab.path_content.selected_path_string() else { + return Ok(()); + }; + let _ = execute_in_child(NITROGEN, &["--set-zoom-fill", "--save", &path_str]); Ok(()) } /// Add a song or a folder to MOC playlist. Start it first... pub fn mocp_add_to_playlist(tab: &Tab) -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } Mocp::add_to_playlist(tab) } + pub fn mocp_clear_playlist() -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } + Mocp::clear() + } + /// Add a song or a folder to MOC playlist. Start it first... pub fn mocp_go_to_song(tab: &mut Tab) -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } Mocp::go_to_song(tab) } @@ -977,23 +1056,35 @@ impl EventAction { /// Starts the server if needed, preventing the output to fill the screen. /// Then toggle play/pause pub fn mocp_toggle_pause(status: &mut Status) -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } Mocp::toggle_pause(status) } /// Skip to the next song in MOC pub fn mocp_next() -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } Mocp::next() } /// Go to the previous song in MOC pub fn mocp_previous() -> Result<()> { + if !is_mocp_installed() { + write_log_line("mocp isn't installed".to_owned()); + return Ok(()); + } Mocp::previous() } /// Execute a custom event on the selected file - pub fn custom(status: &mut Status, string: String) -> Result<()> { + pub fn custom(status: &mut Status, string: &String) -> Result<()> { info!("custom {string}"); - let parser = ShellCommandParser::new(&string); + let parser = ShellCommandParser::new(string); let mut args = parser.compute(status)?; let command = args.remove(0); let args: Vec<&str> = args.iter().map(|s| &**s).collect(); @@ -1001,6 +1092,52 @@ impl EventAction { info!("output {output}"); Ok(()) } + + pub fn remote_mount(tab: &mut Tab) -> Result<()> { + tab.set_mode(Mode::InputSimple(InputSimple::Remote)); + Ok(()) + } +} + +enum NodeCreation { + Newfile, + Newdir, +} + +impl Display for NodeCreation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Newfile => write!(f, "file"), + Self::Newdir => write!(f, "directory"), + } + } +} + +impl NodeCreation { + fn create(&self, tab: &mut Tab) -> Result<()> { + let path = tab + .path_content + .path + .join(sanitize_filename::sanitize(tab.input.string())); + if path.exists() { + write_log_line(format!( + "{self} {path} already exists", + path = path.display() + )); + } else { + match self { + Self::Newdir => { + fs::create_dir_all(&path)?; + } + Self::Newfile => { + fs::File::create(&path)?; + } + } + let log_line = format!("Created new {self}: {path}", path = path.display()); + write_log_line(log_line); + } + tab.refresh_view() + } } /// Methods called when executing something with Enter key. @@ -1050,7 +1187,10 @@ impl LeaveMode { let len = status.selected_non_mut().path_content.content.len(); if let Some((ch, _)) = marks.selected() { if let Some(path_str) = status.selected_non_mut().path_content_str() { - status.marks.new_mark(*ch, path::PathBuf::from(path_str))?; + let p = path::PathBuf::from(path_str); + status.marks.new_mark(*ch, &p)?; + let log_line = format!("Saved mark {ch} -> {p}", p = p.display()); + write_log_line(log_line); } status.selected().window.reset(len); status.selected().input.reset(); @@ -1081,16 +1221,18 @@ impl LeaveMode { /// Nothing is done if the user typed nothing or an invalid permission like /// 955. pub fn chmod(status: &mut Status) -> Result<()> { - if status.selected().input.is_empty() { + if status.selected().input.is_empty() || status.flagged.is_empty() { return Ok(()); } - let permissions: u32 = - u32::from_str_radix(&status.selected().input.string(), 8).unwrap_or(0_u32); + let input_permission = &status.selected().input.string(); + let permissions: u32 = u32::from_str_radix(input_permission, 8).unwrap_or(0_u32); if permissions <= Status::MAX_PERMISSIONS { for path in status.flagged.content.iter() { Status::set_permissions(path, permissions)? } - status.flagged.clear() + status.flagged.clear(); + let log_line = format!("Changed permissions to {input_permission}"); + write_log_line(log_line); } status.selected().refresh_view()?; status.reset_tabs_view() @@ -1106,7 +1248,9 @@ impl LeaveMode { /// If the user selected a directory, we jump inside it. /// Otherwise, we jump to the parent and select the file. pub fn jump(status: &mut Status) -> Result<()> { - let Some(jump_target) = status.flagged.selected() else { return Ok(()) }; + let Some(jump_target) = status.flagged.selected() else { + return Ok(()); + }; let jump_target = jump_target.to_owned(); let target_dir = match jump_target.parent() { Some(parent) => parent, @@ -1127,8 +1271,8 @@ impl LeaveMode { } /// Execute a shell command typed by the user. - /// pipes and redirections aren't NotSupported - /// expansions are supported + /// pipes and redirections aren't supported + /// but expansions are supported pub fn shell(status: &mut Status) -> Result { let shell_command = status.selected_non_mut().input.string(); let mut args = ShellCommandParser::new(&shell_command).compute(status)?; @@ -1143,7 +1287,9 @@ impl LeaveMode { status.ask_password(PasswordKind::SUDO, None, PasswordUsage::SUDOCOMMAND)?; Ok(false) } else { - let Ok(executable) = which(executable) else { return Ok(true); }; + let Ok(executable) = which(executable) else { + return Ok(true); + }; let current_directory = status .selected_non_mut() .directory_of_selected()? @@ -1180,11 +1326,13 @@ impl LeaveMode { original_path.display(), new_path.display() ); - info!(target: "special", + let log_line = format!( "renaming: original: {} - new: {}", original_path.display(), new_path.display() ); + write_log_line(log_line); + fs::rename(original_path, new_path)?; } @@ -1195,15 +1343,7 @@ impl LeaveMode { /// Nothing is done if the file already exists. /// Filename is sanitized before processing. pub fn newfile(tab: &mut Tab) -> Result<()> { - let path = tab - .path_content - .path - .join(sanitize_filename::sanitize(tab.input.string())); - if !path.exists() { - fs::File::create(&path)?; - info!(target: "special", "New file: {path}", path=path.display()); - } - tab.refresh_view() + NodeCreation::Newfile.create(tab) } /// Creates a new directory with input string as name. @@ -1212,15 +1352,7 @@ impl LeaveMode { /// ie. the user can create `newdir` or `newdir/newfolder`. /// Directory name is sanitized before processing. pub fn newdir(tab: &mut Tab) -> Result<()> { - let path = tab - .path_content - .path - .join(sanitize_filename::sanitize(tab.input.string())); - if !path.exists() { - fs::create_dir_all(&path)?; - info!(target: "special", "New directory: {path}", path=path.display()); - } - tab.refresh_view() + NodeCreation::Newdir.create(tab) } /// Tries to execute the selected file with an executable which is read @@ -1353,15 +1485,22 @@ impl LeaveMode { /// Compress the flagged files into an archive. /// Compression method is chosen by the user. /// The archive is created in the current directory and is named "archive.tar.??" or "archive.zip". + /// Files which are above the CWD are filtered out since they can't be added to an archive. + /// Archive creation depends on CWD so we ensure it's set to the selected tab. fn compress(status: &mut Status) -> Result<()> { - let cwd = std::env::current_dir()?; - let files_with_relative_paths = status + let here = &status.selected_non_mut().path_content.path; + std::env::set_current_dir(here)?; + let files_with_relative_paths: Vec = status .flagged .content .iter() - .filter_map(|abs_path| pathdiff::diff_paths(abs_path, &cwd)) + .filter_map(|abs_path| pathdiff::diff_paths(abs_path, here)) + .filter(|f| !f.starts_with("..")) .collect(); - status.compression.compress(files_with_relative_paths) + if files_with_relative_paths.is_empty() { + return Ok(()); + } + status.compression.compress(files_with_relative_paths, here) } /// Execute the selected command. @@ -1369,7 +1508,9 @@ impl LeaveMode { /// context. pub fn command(status: &mut Status, colors: &Colors) -> Result<()> { let command_str = status.selected_non_mut().completion.current_proposition(); - let Ok(command) = ActionMap::from_str(command_str) else { return Ok(()) }; + let Ok(command) = ActionMap::from_str(command_str) else { + return Ok(()); + }; command.matcher(status, colors) } @@ -1395,4 +1536,40 @@ impl LeaveMode { tab.window.reset(tab.path_content.content.len()); Ok(()) } + + /// Run sshfs with typed parameters to mount a remote directory in current directory. + /// sshfs should be reachable in path. + /// The user must type 3 arguments like this : `username hostname remote_path`. + /// If the user doesn't provide 3 arguments, + pub fn remote(tab: &mut Tab) -> Result<()> { + let user_hostname_remotepath_string = tab.input.string(); + let strings: Vec<&str> = user_hostname_remotepath_string.split(' ').collect(); + tab.input.reset(); + + if !is_program_in_path(SSHFS_EXECUTABLE) { + info!("{SSHFS_EXECUTABLE} isn't in path"); + return Ok(()); + } + + if strings.len() != 3 { + info!( + "Wrong number of parameters for {SSHFS_EXECUTABLE}, expected 3, got {nb}", + nb = strings.len() + ); + return Ok(()); + }; + + let (username, hostname, remote_path) = (strings[0], strings[1], strings[2]); + let current_path: &str = tab + .current_directory_path() + .to_str() + .context("couldn't parse the path")?; + let first_arg = &format!("{username}@{hostname}:{remote_path}"); + let command_output = + execute_and_capture_output(SSHFS_EXECUTABLE, &[first_arg, current_path]); + let log_line = format!("{SSHFS_EXECUTABLE} output {command_output:?}"); + info!("{log_line}"); + write_log_line(log_line); + Ok(()) + } } diff --git a/src/fileinfo.rs b/src/fileinfo.rs index 7cf470fa..89412efc 100644 --- a/src/fileinfo.rs +++ b/src/fileinfo.rs @@ -1,4 +1,4 @@ -use std::fs::{metadata, read_dir, DirEntry, Metadata}; +use std::fs::{read_dir, symlink_metadata, DirEntry, Metadata}; use std::iter::Enumerate; use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::path; @@ -18,9 +18,11 @@ use crate::impl_selectable_content; use crate::sort::SortKind; use crate::utils::filename_from_path; +type Valid = bool; + /// Different kind of files #[derive(Debug, Clone, Copy)] -pub enum FileKind { +pub enum FileKind { /// Classic files. NormalFile, /// Folder @@ -34,14 +36,14 @@ pub enum FileKind { /// File socket Socket, /// symlink - SymbolicLink, + SymbolicLink(Valid), } -impl FileKind { +impl FileKind { /// Returns a new `FileKind` depending on metadata. /// Only linux files have some of those metadata /// since we rely on `std::fs::MetadataExt`. - pub fn new(meta: &Metadata) -> Self { + pub fn new(meta: &Metadata, filepath: &path::Path) -> Self { if meta.file_type().is_dir() { Self::Directory } else if meta.file_type().is_block_device() { @@ -53,7 +55,8 @@ impl FileKind { } else if meta.file_type().is_fifo() { Self::Fifo } else if meta.file_type().is_symlink() { - Self::SymbolicLink + let valid = is_valid_symlink(filepath); + Self::SymbolicLink(valid) } else { Self::NormalFile } @@ -69,7 +72,7 @@ impl FileKind { FileKind::NormalFile => '.', FileKind::CharDevice => 'c', FileKind::BlockDevice => 'b', - FileKind::SymbolicLink => 'l', + FileKind::SymbolicLink(_) => 'l', } } @@ -77,7 +80,7 @@ impl FileKind { match self { FileKind::Directory => 'a', FileKind::NormalFile => 'b', - FileKind::SymbolicLink => 'c', + FileKind::SymbolicLink(_) => 'c', FileKind::BlockDevice => 'd', FileKind::CharDevice => 'e', FileKind::Socket => 'f', @@ -86,10 +89,44 @@ impl FileKind { } } +/// Different kind of display for the size column. +/// ls -lh display a human readable size for normal files, +/// nothing should be displayed for a directory, +/// Major & Minor driver versions are used for CharDevice & BlockDevice +#[derive(Clone, Debug)] +pub enum SizeColumn { + /// Used for normal files. It's the size in bytes. + Size(u64), + /// Used for directories, nothing is displayed + None, + /// Use for CharDevice and BlockDevice. + /// It's the major & minor driver versions. + MajorMinor((u8, u8)), +} + +impl std::fmt::Display for SizeColumn { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Size(bytes) => write!(f, " {hs}", hs = human_size(*bytes)), + Self::None => write!(f, " - "), + Self::MajorMinor((major, minor)) => write!(f, "{major:>3},{minor:<3}"), + } + } +} + +impl SizeColumn { + fn new(size: u64, metadata: &Metadata, file_kind: &FileKind) -> Self { + match file_kind { + FileKind::Directory => Self::None, + FileKind::CharDevice | FileKind::BlockDevice => Self::MajorMinor(major_minor(metadata)), + _ => Self::Size(size), + } + } +} + /// Infos about a file /// We read and keep tracks every displayable information about /// a file. -/// Like in [exa](https://github.com/ogham/exa) we don't display the group. #[derive(Clone, Debug)] pub struct FileInfo { /// Full path of the file @@ -97,7 +134,10 @@ pub struct FileInfo { /// Filename pub filename: String, /// File size as a `String`, already human formated. - pub file_size: String, + /// For char devices and block devices we display major & minor like ls. + pub size_column: SizeColumn, + /// True size of a file, not formated + pub true_size: u64, /// Owner name of the file. pub owner: String, /// Group name of the file. @@ -107,7 +147,7 @@ pub struct FileInfo { /// Is this file currently selected ? pub is_selected: bool, /// What kind of file is this ? - pub file_kind: FileKind, + pub file_kind: FileKind, /// Extension of the file. `""` for a directory. pub extension: String, /// A formated filename where the "kind" of file @@ -123,7 +163,7 @@ impl FileInfo { let path = direntry.path(); let filename = extract_filename(direntry)?; - Self::create_from_metadata_and_filename(&path, filename, users_cache) + Self::create_from_metadata_and_filename(&path, &filename, users_cache) } /// Creates a fileinfo from a path and a filename. @@ -133,7 +173,7 @@ impl FileInfo { filename: &str, users_cache: &UsersCache, ) -> Result { - Self::create_from_metadata_and_filename(path, filename.to_owned(), users_cache) + Self::create_from_metadata_and_filename(path, filename, users_cache) } pub fn from_path(path: &path::Path, users_cache: &UsersCache) -> Result { @@ -141,17 +181,12 @@ impl FileInfo { .file_name() .context("from path: couldn't read filenale")? .to_str() - .context("from path: couldn't parse filenale")? - .to_owned(); + .context("from path: couldn't parse filenale")?; Self::create_from_metadata_and_filename(path, filename, users_cache) } fn metadata(&self) -> Result { - Ok(metadata(&self.path)?) - } - - pub fn size(&self) -> Result { - Ok(extract_file_size(&self.metadata()?)) + Ok(symlink_metadata(&self.path)?) } pub fn permissions(&self) -> Result { @@ -160,25 +195,29 @@ impl FileInfo { fn create_from_metadata_and_filename( path: &path::Path, - filename: String, + filename: &str, users_cache: &UsersCache, ) -> Result { - let metadata = metadata(path)?; + let filename = filename.to_owned(); + let metadata = symlink_metadata(path)?; let path = path.to_owned(); let owner = extract_owner(&metadata, users_cache)?; let group = extract_group(&metadata, users_cache)?; let system_time = extract_datetime(&metadata)?; let is_selected = false; - let file_size = human_size(extract_file_size(&metadata)); + let true_size = extract_file_size(&metadata); + + let file_kind = FileKind::new(&metadata, &path); - let file_kind = FileKind::new(&metadata); + let size_column = SizeColumn::new(true_size, &metadata, &file_kind); let extension = extract_extension(&path).into(); let kind_format = filekind_and_filename(&filename, &file_kind); Ok(FileInfo { path, filename, - file_size, + size_column, + true_size, owner, group, system_time, @@ -188,29 +227,44 @@ impl FileInfo { kind_format, }) } + /// Format the file line. /// Since files can have different owners in the same directory, we need to /// know the maximum size of owner column for formatting purpose. pub fn format(&self, owner_col_width: usize, group_col_width: usize) -> Result { - let mut repr = format!( - "{dir_symbol}{permissions} {file_size} {owner: repr.push_str(&format!(" -> {dest}")), + None => repr.push_str(" broken link"), + } + } + Ok(repr) + } + + fn format_base(&self, owner_col_width: usize, group_col_width: usize) -> Result { + let repr = format!( + "{dir_symbol}{permissions} {file_size} {owner: "); - repr.push_str(&self.read_dest().unwrap_or_else(|| "Broken link".to_owned())); - } Ok(repr) } + /// Format the metadata line, without the filename. + /// Owned & Group have fixed width of 6. + pub fn format_no_filename(&self) -> Result { + self.format_base(6, 6) + } + pub fn dir_symbol(&self) -> char { self.file_kind.extract_dir_symbol() } @@ -219,13 +273,6 @@ impl FileInfo { Ok(self.filename.to_owned()) } - fn read_dest(&self) -> Option { - match metadata(&self.path) { - Ok(_) => Some(std::fs::read_link(&self.path).ok()?.to_str()?.to_owned()), - Err(_) => Some("Broken link".to_owned()), - } - } - /// Select the file. pub fn select(&mut self) { self.is_selected = true; @@ -243,6 +290,16 @@ impl FileInfo { pub fn is_dir(&self) -> bool { self.path.is_dir() } + + /// Name of proper files, empty string for `.` and `..`. + pub fn filename_without_dot_dotdot(&self) -> String { + let name = &self.filename; + if name == "." || name == ".." { + "".to_owned() + } else { + format!("/{name} ") + } + } } /// Holds the information about file in the current directory. @@ -338,8 +395,8 @@ impl PathContent { /// Convert a path to a &str. /// It may fails if the path contains non valid utf-8 chars. - pub fn path_to_str(&self) -> Result<&str> { - self.path.to_str().context("path to str: Unreadable path") + pub fn path_to_str(&self) -> String { + self.path.display().to_string() } /// Sort the file with current key. @@ -386,7 +443,7 @@ impl PathContent { /// Path of the currently selected file. pub fn selected_path_string(&self) -> Option { - Some(self.selected()?.path.to_str()?.to_owned()) + Some(self.selected()?.path.display().to_string()) } /// True if the path starts with a subpath. @@ -403,14 +460,17 @@ impl PathContent { .file_kind { FileKind::Directory => Ok(true), - FileKind::SymbolicLink => { - let dest = self - .selected() - .context("is selected dir: unreachable")? - .read_dest() - .unwrap_or_default(); + FileKind::SymbolicLink(true) => { + let dest = read_symlink_dest( + &self + .selected() + .context("is selected dir: unreachable")? + .path, + ) + .unwrap_or_default(); Ok(path::PathBuf::from(dest).is_dir()) } + FileKind::SymbolicLink(false) => Ok(false), _ => Ok(false), } } @@ -501,7 +561,8 @@ pub fn fileinfo_attr(fileinfo: &FileInfo, colors: &Colors) -> Attr { FileKind::CharDevice => str_to_tuikit(&colors.char), FileKind::Fifo => str_to_tuikit(&colors.fifo), FileKind::Socket => str_to_tuikit(&colors.socket), - FileKind::SymbolicLink => str_to_tuikit(&colors.symlink), + FileKind::SymbolicLink(true) => str_to_tuikit(&colors.symlink), + FileKind::SymbolicLink(false) => str_to_tuikit(&colors.broken), _ => colors.color_cache.extension_color(&fileinfo.extension), }; @@ -549,11 +610,7 @@ fn extract_permissions_string(metadata: &Metadata) -> String { let s_o = convert_octal_mode(mode >> 6); let s_g = convert_octal_mode((mode >> 3) & 7); let s_a = convert_octal_mode(mode & 7); - let mut perm = String::with_capacity(9); - perm.push_str(s_o); - perm.push_str(s_a); - perm.push_str(s_g); - perm + format!("{s_o}{s_a}{s_g}") } /// Convert an integer like `Oo7` into its string representation like `"rwx"` @@ -605,6 +662,15 @@ pub fn human_size(bytes: u64) -> String { ) } +/// Extract the major & minor driver version of a special file. +/// It's used for CharDevice & BlockDevice +fn major_minor(metadata: &Metadata) -> (u8, u8) { + let device_ids = metadata.rdev().to_be_bytes(); + let major = device_ids[6]; + let minor = device_ids[7]; + (major, minor) +} + /// Extract the optional extension from a filename. /// Returns empty &str aka "" if the file has no extension. pub fn extract_extension(path: &path::Path) -> &str { @@ -614,14 +680,11 @@ pub fn extract_extension(path: &path::Path) -> &str { } fn get_used_space(files: &[FileInfo]) -> u64 { - files.iter().map(|f| f.size().unwrap_or_default()).sum() + files.iter().map(|f| f.true_size).sum() } -fn filekind_and_filename(filename: &str, file_kind: &FileKind) -> String { - let mut s = String::new(); - s.push(file_kind.sortable_char()); - s.push_str(filename); - s +fn filekind_and_filename(filename: &str, file_kind: &FileKind) -> String { + format!("{c}{filename}", c = file_kind.sortable_char()) } /// Creates an optional vector of fileinfo contained in a file. @@ -646,9 +709,8 @@ pub fn files_collection( ), Err(error) => { info!( - "Couldn't read path {} - {}", - fileinfo.path.to_string_lossy(), - error + "Couldn't read path {path} - {error}", + path = fileinfo.path.display(), ); None } @@ -687,3 +749,19 @@ pub fn shorten_path(path: &path::Path, size: Option) -> Result { .collect(); Ok(shortened_elems.join("/")) } + +/// Returns `Some(destination)` where `destination` is a String if the path is +/// the destination of a symlink, +/// Returns `None` if the link is broken, if the path doesn't exists or if the path +/// isn't a symlink. +fn read_symlink_dest(path: &path::Path) -> Option { + match std::fs::read_link(path) { + Ok(dest) if dest.exists() => Some(dest.to_str()?.to_owned()), + _ => None, + } +} + +/// true iff the path is a valid symlink (pointing to an existing file). +fn is_valid_symlink(path: &path::Path) -> bool { + matches!(std::fs::read_link(path), Ok(dest) if dest.exists()) +} diff --git a/src/git.rs b/src/git.rs index 40a03ea8..136a72b6 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,14 +1,18 @@ // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Copied from https://github.com/9ary/gitprompt-rs/blob/master/src/main.rs +// Copied and modified from https://github.com/9ary/gitprompt-rs/blob/master/src/main.rs // Couldn't use without forking and I'm lazy. -use anyhow::{anyhow, Context, Result}; use std::fmt::Write as _; use std::path::Path; use std::process; +use anyhow::{anyhow, Context, Result}; + +use crate::utils::is_program_in_path; + +#[derive(Default)] struct GitStatus { branch: Option, ahead: i64, @@ -21,68 +25,103 @@ struct GitStatus { untracked: i64, } -fn parse_porcelain2(data: String) -> Option { - let mut status = GitStatus { - branch: None, - ahead: 0, - behind: 0, - - staged: 0, - modified: 0, - deleted: 0, - unmerged: 0, - untracked: 0, - }; - // Simple parser for the porcelain v2 format - for entry in data.split('\0') { - let mut entry = entry.split(' '); - match entry.next() { - // Header lines - Some("#") => match entry.next()? { - "branch.head" => { - let head = entry.next()?; - if head != "(detached)" { - status.branch = Some(String::from(head)); +impl GitStatus { + fn parse_porcelain2(porcerlain2_output: String) -> Option { + let mut status = GitStatus::default(); + // Simple parser for the porcelain v2 format + for entry in porcerlain2_output.split('\0') { + let mut entry = entry.split(' '); + match entry.next() { + // Header lines + Some("#") => match entry.next()? { + "branch.head" => { + let head = entry.next()?; + if head != "(detached)" { + status.branch = Some(String::from(head)); + } + } + "branch.ab" => { + let a = entry.next()?; + let b = entry.next()?; + status.ahead = a.parse::().ok()?.abs(); + status.behind = b.parse::().ok()?.abs(); } - } - "branch.ab" => { - let a = entry.next()?; - let b = entry.next()?; - status.ahead = a.parse::().ok()?.abs(); - status.behind = b.parse::().ok()?.abs(); - } - _ => {} - }, - // File entries - Some("1") | Some("2") => { - let mut xy = entry.next()?.chars(); - let x = xy.next()?; - let y = xy.next()?; - if x != '.' { - status.staged += 1; - } - match y { - 'M' => status.modified += 1, - 'D' => status.deleted += 1, _ => {} + }, + // File entries + Some("1") | Some("2") => { + let mut xy = entry.next()?.chars(); + let x = xy.next()?; + let y = xy.next()?; + if x != '.' { + status.staged += 1; + } + match y { + 'M' => status.modified += 1, + 'D' => status.deleted += 1, + _ => {} + } } + Some("u") => status.unmerged += 1, + Some("?") => status.untracked += 1, + _ => {} } - Some("u") => status.unmerged += 1, - Some("?") => status.untracked += 1, - _ => {} } + Some(status) } - Some(status) -} -/// Returns a string representation of the git status of this path. -/// Will return an empty string if we're not in a git repository. -pub fn git(path: &Path) -> Result { - if std::env::set_current_dir(path).is_err() { - // The path may not exist. It should never happen. - return Ok("".to_owned()); + fn is_modified(&self) -> bool { + self.untracked + self.modified + self.deleted + self.unmerged + self.staged > 0 } - let output = process::Command::new("git") + + fn format_git_string(&self) -> Result { + let mut git_string = String::new(); + + git_string.push('('); + + if let Some(branch) = &self.branch { + git_string.push_str(branch); + } else { + // Detached head + git_string.push_str(":HEAD"); + } + + // Divergence with remote branch + if self.ahead != 0 { + write!(git_string, "↑{}", self.ahead)?; + } + if self.behind != 0 { + write!(git_string, "↓{}", self.behind)?; + } + + if self.is_modified() { + git_string.push('|'); + + if self.untracked != 0 { + write!(git_string, "+{}", self.untracked)?; + } + if self.modified != 0 { + write!(git_string, "~{}", self.modified)?; + } + if self.deleted != 0 { + write!(git_string, "-{}", self.deleted)?; + } + if self.unmerged != 0 { + write!(git_string, "x{}", self.unmerged)?; + } + if self.staged != 0 { + write!(git_string, "•{}", self.staged)?; + } + } + + git_string.push(')'); + + Ok(git_string) + } +} + +fn porcelain2() -> Result { + process::Command::new("git") .args([ "status", "--porcelain=v2", @@ -92,58 +131,31 @@ pub fn git(path: &Path) -> Result { ]) .stdin(process::Stdio::null()) .stderr(process::Stdio::null()) - .output()?; + .output() +} + +/// Returns a string representation of the git status of this path. +/// Will return an empty string if we're not in a git repository. +pub fn git(path: &Path) -> Result { + if !is_program_in_path("git") { + return Ok("".to_owned()); + } + if std::env::set_current_dir(path).is_err() { + // The path may not exist. It should never happen. + return Ok("".to_owned()); + } + let output = porcelain2()?; if !output.status.success() { // We're most likely not in a Git repo return Ok("".to_owned()); } - let status = String::from_utf8(output.stdout) + let porcerlain_output = String::from_utf8(output.stdout) .ok() .context("Invalid UTF-8 while decoding Git output")?; - let status = parse_porcelain2(status).context("Error while parsing Git output")?; - - let mut git_string = String::new(); - - git_string.push('('); - - if let Some(branch) = status.branch { - git_string.push_str(&branch); - } else { - // Detached head - git_string.push_str(":HEAD"); - } - - // Divergence with remote branch - if status.ahead != 0 { - write!(git_string, "↑{}", status.ahead)?; - } - if status.behind != 0 { - write!(git_string, "↓{}", status.ahead)?; - } - - if status.untracked + status.modified + status.deleted + status.unmerged + status.staged > 0 { - git_string.push('|'); - } - if status.untracked != 0 { - write!(git_string, "+{}", status.untracked)?; - } - if status.modified != 0 { - write!(git_string, "~{}", status.modified)?; - } - if status.deleted != 0 { - write!(git_string, "-{}", status.deleted)?; - } - if status.unmerged != 0 { - write!(git_string, "x{}", status.unmerged)?; - } - if status.staged != 0 { - write!(git_string, "•{}", status.staged)?; - } - - git_string.push(')'); - - Ok(git_string) + GitStatus::parse_porcelain2(porcerlain_output) + .context("Error while parsing Git output")? + .format_git_string() } /// Returns the git root. diff --git a/src/help.rs b/src/help.rs index 2c93236f..9efff9fc 100644 --- a/src/help.rs +++ b/src/help.rs @@ -103,6 +103,7 @@ Navigate as usual. Most actions works as in 'normal' view. {Command:<10}: COMMAND {Bulk:<10}: BULK {ShellMenu:<10}: SHELL MENU +{RemoteMount:<10}: MOUNT REMOTE PATH {Filter:<10}: FILTER (by name \"n name\", by ext \"e ext\", only directories d or all for reset) {Enter:<10}: Execute mode then NORMAL @@ -110,11 +111,12 @@ Navigate as usual. Most actions works as in 'normal' view. - MOC - Control MOC from your TUI -{MocpAddToPlayList:<10}: MOCP: Add a file or folder to the playlist +{MocpAddToPlayList:<10}: MOCP: Add selected file or folder to the playlist {MocpPrevious:<10}: MOCP: Previous song {MocpTogglePause:<10}: MOCP: Toggle play/pause. {MocpNext:<10}: MOCP: Next song {MocpGoToSong:<10}: MOCP: Go to currently playing song +{MocpClearPlaylist:<10}: MOCP: Clear the playlist "; const CUSTOM_HELP: &str = " diff --git a/src/iso.rs b/src/iso.rs index 3c5c0c5c..575e3e60 100644 --- a/src/iso.rs +++ b/src/iso.rs @@ -138,7 +138,7 @@ impl MountHelper for IsoDevice { fn as_string(&self) -> Result { if let Some(ref mount_point) = self.mountpoints { Ok(format!( - "mounted {}\nto {}", + "mounted {} to {}", self.path, mount_point.display() )) diff --git a/src/keybindings.rs b/src/keybindings.rs index 155e0e55..5562a513 100644 --- a/src/keybindings.rs +++ b/src/keybindings.rs @@ -103,6 +103,7 @@ impl Bindings { (Key::Alt('l'), ActionMap::Log), (Key::Alt('o'), ActionMap::TrashOpen), (Key::Alt('p'), ActionMap::TogglePreviewSecond), + (Key::Alt('r'), ActionMap::RemoteMount), (Key::Alt('x'), ActionMap::TrashEmpty), (Key::Alt('z'), ActionMap::TreeFoldAll), (Key::Ctrl('c'), ActionMap::CopyFilename), @@ -115,6 +116,7 @@ impl Bindings { (Key::Ctrl('p'), ActionMap::CopyFilepath), (Key::Ctrl('q'), ActionMap::ResetMode), (Key::Ctrl('r'), ActionMap::RefreshView), + (Key::Ctrl('x'), ActionMap::MocpClearPlaylist), (Key::AltEnter, ActionMap::MocpGoToSong), (Key::CtrlUp, ActionMap::MocpAddToPlayList), (Key::CtrlDown, ActionMap::MocpTogglePause), @@ -143,9 +145,11 @@ impl Bindings { /// It may fail (and leave keybinding intact) if the file isn't formated properly. /// An unknown or poorly formated key will be ignored. pub fn update_normal(&mut self, yaml: &serde_yaml::value::Value) { - let Some(mappings) = yaml.as_mapping() else { return }; + let Some(mappings) = yaml.as_mapping() else { + return; + }; for yaml_key in mappings.keys() { - let Some(key_string) = yaml_key.as_str() else { + let Some(key_string) = yaml_key.as_str() else { log::info!("~/.config/fm/config.yaml: Keybinding {yaml_key:?} is unreadable"); continue; }; @@ -153,8 +157,10 @@ impl Bindings { log::info!("~/.config/fm/config.yaml: Keybinding {key_string} is unknown"); continue; }; - let Some(action_str) = yaml[yaml_key].as_str() else { continue; }; - let Ok(action) = ActionMap::from_str(action_str) else { + let Some(action_str) = yaml[yaml_key].as_str() else { + continue; + }; + let Ok(action) = ActionMap::from_str(action_str) else { log::info!("~/.config/fm/config.yaml: Action {action_str} is unknown"); continue; }; @@ -163,10 +169,12 @@ impl Bindings { } pub fn update_custom(&mut self, yaml: &serde_yaml::value::Value) { - let Some(mappings) = yaml.as_mapping() else { return }; + let Some(mappings) = yaml.as_mapping() else { + return; + }; let mut custom = vec![]; for yaml_key in mappings.keys() { - let Some(key_string) = yaml_key.as_str() else { + let Some(key_string) = yaml_key.as_str() else { log::info!("~/.config/fm/config.yaml: Keybinding {yaml_key:?} is unreadable"); continue; }; @@ -174,7 +182,9 @@ impl Bindings { log::info!("~/.config/fm/config.yaml: Keybinding {key_string} is unknown"); continue; }; - let Some(custom_str) = yaml[yaml_key].as_str() else { continue; }; + let Some(custom_str) = yaml[yaml_key].as_str() else { + continue; + }; let action = ActionMap::Custom(custom_str.to_owned()); log::info!("custom bind {keymap:?}, {action}"); self.binds.insert(keymap, action.clone()); diff --git a/src/log.rs b/src/log.rs index a5a95186..f917590b 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,9 +1,14 @@ +use std::sync::RwLock; + use anyhow::Result; +use lazy_static::lazy_static; use log4rs; use crate::constant_strings_paths::{ACTION_LOG_PATH, LOG_CONFIG_PATH}; use crate::utils::extract_lines; +// static ENV_HOME: &str = "$ENV{HOME}"; + /// Set the logs. /// The configuration is read from a config file defined in `LOG_CONFIG_PATH` /// It's a YAML file which defines 2 logs: @@ -14,12 +19,71 @@ pub fn set_loggers() -> Result<()> { shellexpand::tilde(LOG_CONFIG_PATH).as_ref(), Default::default(), )?; + // clear_useless_env_home()?; + + log::info!("fm is starting"); Ok(()) } +/// Delete useless $ENV{HOME} folder created by log4rs. +/// This folder is created when a log file is big enough to proc a rolling +/// Since the pattern can't be resolved, it's not created in the config folder but where the app is started... +/// See [github issue](https://github.com/estk/log4rs/issues/314) +/// The function log its results and delete nothing. +// fn clear_useless_env_home() -> Result<()> { +// let p = std::path::Path::new(&ENV_HOME); +// let cwd = std::env::current_dir(); +// log::info!( +// "looking from {ENV_HOME} - {p} CWD {cwd}", +// p = p.display(), +// cwd = cwd?.display() +// ); +// if p.exists() && std::fs::metadata(ENV_HOME)?.is_dir() +// // && std::path::Path::new(ENV_HOME).read_dir()?.next().is_none() +// { +// let z = std::path::Path::new(ENV_HOME).read_dir()?.next(); +// log::info!("z {z:?}"); +// +// // std::fs::remove_dir_all(ENV_HOME)?; +// log::info!("Removed {ENV_HOME} empty directory from CWD"); +// } +// Ok(()) +// } + /// Returns the last line of the log file. pub fn read_log() -> Result> { let log_path = shellexpand::tilde(ACTION_LOG_PATH).to_string(); let content = std::fs::read_to_string(log_path)?; Ok(extract_lines(content)) } + +lazy_static! { + static ref LAST_LOG_LINE: RwLock = RwLock::new("".to_string()); +} + +/// Read the last value of the "log line". +/// It's a global string created with `lazy_static!(...)` +/// Fail silently if the global variable can't be read and returns an empty string. +pub fn read_last_log_line() -> String { + let Ok(last_log_line) = LAST_LOG_LINE.read() else { + return "".to_owned(); + }; + last_log_line.to_string() +} + +/// Write a new log line to the global variable `LAST_LOG_LINE`. +/// It uses `lazy_static` to manipulate the global variable. +/// Fail silently if the global variable can't be written. +fn write_last_log_line(log: &str) { + let Ok(mut new_log_line) = LAST_LOG_LINE.write() else { + return; + }; + *new_log_line = log.to_owned(); +} + +/// Write a line to both the global variable `LAST_LOG_LINE` and the special log +/// which can be displayed with Alt+l +pub fn write_log_line(log_line: String) { + log::info!(target: "special", "{log_line}"); + write_last_log_line(&log_line); +} diff --git a/src/main.rs b/src/main.rs index 47a91479..364ea1f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,76 +1,145 @@ use std::sync::Arc; use anyhow::Result; -use clap::Parser; -use fm::opener::{load_opener, Opener}; use log::info; -use fm::args::Args; -use fm::config::load_config; +use fm::config::{load_config, Colors}; use fm::constant_strings_paths::{CONFIG_PATH, OPENER_PATH}; use fm::event_dispatch::EventDispatcher; use fm::help::Help; use fm::log::set_loggers; +use fm::opener::{load_opener, Opener}; use fm::status::Status; use fm::term_manager::{Display, EventReader}; -use fm::utils::{drop_everything, init_term, print_on_quit}; - -/// Main function -/// Init the status and display and listen to events (keyboard, mouse, resize, custom...). -/// The application is redrawn after every event. -/// When the user issues a quit event, the main loop is broken and we reset the cursor. -fn main() -> Result<()> { - set_loggers()?; +use fm::utils::{init_term, print_on_quit}; - info!("fm is starting"); +/// Holds everything about the application itself. +/// Most attributes holds an `Arc`. +/// Dropping the instance of FM allows to write again to stdout. +struct FM { + /// Poll the event sent to the terminal by the user or the OS + event_reader: EventReader, + /// Associate the event to a method, modifing the status. + event_dispatcher: EventDispatcher, + /// Current status of the application. Mostly the filetrees + status: Status, + /// Responsible for the display on screen. + display: Display, + /// Colors used by different kind of files. + /// Since most are generated the first time an extension is met, + /// we need to hold this. + colors: Colors, +} - let Ok(config) = load_config(CONFIG_PATH) else { - eprintln!("Couldn't load the config file at {CONFIG_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/config.yaml for an example."); - info!("Couldn't read the config file {CONFIG_PATH}"); - std::process::exit(1); - }; - info!("config loaded"); - let term = Arc::new(init_term()?); - let event_dispatcher = EventDispatcher::new(config.binds.clone()); - let event_reader = EventReader::new(term.clone()); - let opener = load_opener(OPENER_PATH, &config.terminal).unwrap_or_else(|_| { +impl FM { + /// Setup everything the application needs in its main loop : + /// an `EventReader`, + /// an `EventDispatcher`, + /// a `Status`, + /// a `Display`, + /// some `Colors`. + /// It reads and drops the configuration from the config file. + /// If the config can't be parsed, it exits with error code 1. + fn start() -> Result { + let Ok(config) = load_config(CONFIG_PATH) else { + exit_wrong_config() + }; + let term = Arc::new(init_term()?); + let event_reader = EventReader::new(term.clone()); + let event_dispatcher = EventDispatcher::new(config.binds.clone()); + let opener = load_opener(OPENER_PATH, &config.terminal).unwrap_or_else(|_| { eprintln!("Couldn't read the opener config file at {OPENER_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/opener.yaml for an example. Using default."); info!("Couldn't read opener file at {OPENER_PATH}. Using default."); Opener::new(&config.terminal) }); - let help = Help::from_keybindings(&config.binds, &opener)?.help; - let mut display = Display::new(term.clone()); - let mut status = Status::new( - Args::parse(), - display.height()?, - term.clone(), - help, - opener, - &config.settings, - )?; - let colors = config.colors.clone(); - drop(config); + let help = Help::from_keybindings(&config.binds, &opener)?.help; + let display = Display::new(term.clone()); + let status = Status::new(display.height()?, term, help, opener, &config.settings)?; + let colors = config.colors.clone(); + drop(config); + Ok(Self { + event_reader, + event_dispatcher, + status, + display, + colors, + }) + } - info!(target: "special", "config dropped"); + /// Return the last event received by the terminal + fn poll_event(&mut self) -> Result { + self.event_reader.poll_event() + } - while let Ok(event) = event_reader.poll_event() { - event_dispatcher.dispatch(&mut status, event, &colors)?; - status.refresh_disks(); - if status.force_clear { - display.force_clear()?; - status.force_clear = false; + /// Force clear the display if the status requires it, then reset it in status. + fn force_clear_if_needed(&mut self) -> Result<()> { + if self.status.force_clear { + self.display.force_clear()?; + self.status.force_clear = false; } - display.display_all(&status, &colors)?; + Ok(()) + } + + /// Update itself, changing its status. + fn update(&mut self, event: tuikit::prelude::Event) -> Result<()> { + self.event_dispatcher.dispatch( + &mut self.status, + event, + &self.colors, + self.event_reader.term_height()?, + )?; + self.status.refresh_disks(); + Ok(()) + } + + /// Display itself using its `display` attribute. + fn display(&mut self) -> Result<()> { + self.force_clear_if_needed()?; + self.display.display_all(&self.status, &self.colors) + } + + /// True iff the application must quit. + fn must_quit(&self) -> bool { + self.status.must_quit() + } + + /// Display the cursor, + /// drop itself, which allow us to print normally afterward + /// print the final path + fn quit(self) -> Result<()> { + self.display.show_cursor()?; + let final_path = self.status.selected_path_str().to_owned(); + drop(self); + print_on_quit(&final_path); + info!("fm is shutting down"); + Ok(()) + } +} + +/// Exit the application and log a message. +/// Used when the config can't be read. +fn exit_wrong_config() -> ! { + eprintln!("Couldn't load the config file at {CONFIG_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/config.yaml for an example."); + info!("Couldn't read the config file {CONFIG_PATH}"); + std::process::exit(1) +} - if status.must_quit() { +/// Main function +/// Init the status and display and listen to events (keyboard, mouse, resize, custom...). +/// The application is redrawn after every event. +/// When the user issues a quit event, the main loop is broken +/// Then we reset the cursor, drop everything holding a terminal and print the last path. +fn main() -> Result<()> { + set_loggers()?; + let mut fm = FM::start()?; + + while let Ok(event) = fm.poll_event() { + fm.update(event)?; + fm.display()?; + if fm.must_quit() { break; - }; + } } - display.show_cursor()?; - let final_path = status.selected_path_str().to_owned(); - drop_everything(term, event_dispatcher, event_reader, status, display); - print_on_quit(&final_path); - info!("fm is shutting down"); - Ok(()) + fm.quit() } diff --git a/src/marks.rs b/src/marks.rs index f8f83a2a..e6e6db85 100644 --- a/src/marks.rs +++ b/src/marks.rs @@ -7,6 +7,7 @@ use log::info; use crate::constant_strings_paths::MARKS_FILEPATH; use crate::impl_selectable_content; +use crate::log::write_log_line; use crate::utils::read_lines; /// Holds the marks created by the user. @@ -97,9 +98,11 @@ impl Marks { /// Store a new mark in the config file. /// If an update is done, the marks are saved again. - pub fn new_mark(&mut self, ch: char, path: PathBuf) -> Result<()> { + pub fn new_mark(&mut self, ch: char, path: &Path) -> Result<()> { if ch == ':' { - return Err(anyhow!("new_mark ':' can't be used as a mark")); + let log_line = "new mark - ':' can't be used as a mark"; + write_log_line(log_line.to_owned()); + return Err(anyhow!(log_line)); } if self.used_chars.contains(&ch) { let mut found_index = None; @@ -109,12 +112,17 @@ impl Marks { break; } } - let Some(found_index) = found_index else {return Ok(())}; - self.content[found_index] = (ch, path); + let Some(found_index) = found_index else { + return Ok(()); + }; + self.content[found_index] = (ch, path.to_path_buf()); } else { - self.content.push((ch, path)) + self.content.push((ch, path.to_path_buf())) } + let log_line = format!("Saved mark {ch} -> {p}", p = path.display()); + write_log_line(log_line); + self.save_marks() } @@ -143,7 +151,7 @@ impl Marks { } fn format_mark(ch: &char, path: &Path) -> String { - format!("{} {}", ch, path.to_string_lossy()) + format!("{ch} {path}", path = path.display()) } } diff --git a/src/mocp.rs b/src/mocp.rs index c965f1dd..85d82ba4 100644 --- a/src/mocp.rs +++ b/src/mocp.rs @@ -1,11 +1,22 @@ use anyhow::Result; use log::info; +use crate::constant_strings_paths::DEFAULT_AUDIO_OPENER; use crate::opener::{ execute_and_capture_output, execute_and_capture_output_without_check, execute_in_child, }; use crate::status::Status; use crate::tab::Tab; +use crate::utils::is_program_in_path; + +static MOCP: &str = DEFAULT_AUDIO_OPENER.0; + +/// True iff `mocp` is in path. +/// Nothing can be done here without it, we shouldn't run commands +/// that will always fail. +pub fn is_mocp_installed() -> bool { + is_program_in_path(MOCP) +} /// A bunch of methods to control MOC. /// It relies on the application `mocp` itself to : @@ -21,20 +32,28 @@ pub struct Mocp {} impl Mocp { /// Add a song or a folder to MOC playlist. Start it first... pub fn add_to_playlist(tab: &Tab) -> Result<()> { - let _ = execute_in_child("mocp", &["-S"]); - let Some(path_str) = tab.path_content.selected_path_string() else { return Ok(()); }; + let _ = execute_and_capture_output_without_check(MOCP, &["-S"]); + let Some(path_str) = tab.path_content.selected_path_string() else { + return Ok(()); + }; info!("mocp add to playlist {path_str:?}"); - let _ = execute_in_child("mocp", &["-a", &path_str]); + let _ = execute_and_capture_output_without_check(MOCP, &["-a", &path_str]); Ok(()) } /// Move to the currently playing song. pub fn go_to_song(tab: &mut Tab) -> Result<()> { - let output = execute_and_capture_output_without_check("mocp", &["-Q", "%file"])?; + let output = execute_and_capture_output_without_check(MOCP, &["-Q", "%file"])?; let filepath = std::path::PathBuf::from(output.trim()); - let Some(parent) = filepath.parent() else { return Ok(()) }; - let Some(filename) = filepath.file_name() else { return Ok(()) }; - let Some(filename) = filename.to_str() else { return Ok(()) }; + let Some(parent) = filepath.parent() else { + return Ok(()); + }; + let Some(filename) = filepath.file_name() else { + return Ok(()); + }; + let Some(filename) = filename.to_str() else { + return Ok(()); + }; tab.set_pathcontent(parent)?; tab.search_from(filename, 0); Ok(()) @@ -45,29 +64,29 @@ impl Mocp { /// Then toggle play/pause pub fn toggle_pause(status: &mut Status) -> Result<()> { info!("mocp toggle pause"); - match execute_and_capture_output("mocp", &["-i"]) { + match execute_and_capture_output(MOCP, &["-i"]) { Ok(stdout) => { // server is runing if stdout.contains("STOP") { // music is stopped, start playing music - let _ = execute_and_capture_output("mocp", &["-p"]); + let _ = execute_and_capture_output(MOCP, &["-p"]); } else { // music is playing or paused, toggle play/pause - let _ = execute_and_capture_output("mocp", &["-G"]); + let _ = execute_and_capture_output(MOCP, &["-G"]); } } Err(e) => { status.force_clear(); info!("mocp -i error:\n{e:?}"); // server is stopped, start it. - let c = execute_in_child("mocp", &["-S"]); + let c = execute_in_child(MOCP, &["-S"]); let Ok(mut c) = c else { // it shouldn't fail, something is wrong. It's better not to do anything. - return Ok(()) + return Ok(()); }; let _ = c.wait(); // start playing music - let _ = execute_and_capture_output("mocp", &["-p"]); + let _ = execute_and_capture_output(MOCP, &["-p"]); } } Ok(()) @@ -76,14 +95,26 @@ impl Mocp { /// Skip to the next song in MOC pub fn next() -> Result<()> { info!("mocp next"); - let _ = execute_in_child("mocp", &["-f"]); + let _ = execute_and_capture_output_without_check(MOCP, &["-f"]); Ok(()) } /// Go to the previous song in MOC pub fn previous() -> Result<()> { info!("mocp previous"); - let _ = execute_in_child("mocp", &["-r"]); + let _ = execute_and_capture_output_without_check(MOCP, &["-r"]); + Ok(()) + } + + /// Clear the playlist + /// Since clearing the playlist exit the server, + /// we have to restart it afterwards. + pub fn clear() -> Result<()> { + info!("mocp clear"); + // Clear the playlist **and exit** + let _ = execute_and_capture_output_without_check(MOCP, &["-c"]); + // Restart the server + let _ = execute_and_capture_output_without_check(MOCP, &["-S"]); Ok(()) } } diff --git a/src/mode.rs b/src/mode.rs index ac6ba7d3..2919bb0a 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1,6 +1,10 @@ use std::fmt; use crate::completion::InputCompleted; +use crate::constant_strings_paths::{ + CHMOD_LINES, FILTER_LINES, NEWDIR_LINES, NEWFILE_LINES, NVIM_ADDRESS_LINES, PASSWORD_LINES, + REGEX_LINES, REMOTE_LINES, RENAME_LINES, SHELL_LINES, SORT_LINES, +}; use crate::cryptsetup::BlockDeviceAction; use crate::password::{PasswordKind, PasswordUsage}; @@ -34,11 +38,22 @@ impl NeedConfirmation { /// Since we ask the user confirmation, we need to know how much space /// is needed. pub fn cursor_offset(&self) -> usize { + self.to_string().len() + 8 + } + + /// A confirmation message to be displayed before executing the mode. + /// When files are moved or copied the destination is displayed. + pub fn confirmation_string(&self, destination: &str) -> String { match *self { - Self::Copy => 25, - Self::Delete => 21, - Self::Move => 25, - Self::EmptyTrash => 35, + NeedConfirmation::Copy => { + format!("Files will be copied to {}", destination) + } + NeedConfirmation::Delete | NeedConfirmation::EmptyTrash => { + "Files will be deleted permanently".to_owned() + } + NeedConfirmation::Move => { + format!("Files will be moved to {}", destination) + } } } } @@ -81,6 +96,28 @@ pub enum InputSimple { Password(PasswordKind, Option, PasswordUsage), /// Shell command execute as is Shell, + /// Mount a remote directory with sshfs + Remote, +} + +impl InputSimple { + /// Returns a vector of static &str describing what + /// the mode does. + pub fn lines(&self) -> &'static [&'static str] { + match *self { + Self::Chmod => &CHMOD_LINES, + Self::Filter => &FILTER_LINES, + Self::Newdir => &NEWDIR_LINES, + Self::Newfile => &NEWFILE_LINES, + Self::Password(_, _, _) => &PASSWORD_LINES, + Self::RegexMatch => ®EX_LINES, + Self::Rename => &RENAME_LINES, + Self::SetNvimAddr => &NVIM_ADDRESS_LINES, + Self::Shell => &SHELL_LINES, + Self::Sort => &SORT_LINES, + Self::Remote => &REMOTE_LINES, + } + } } /// Different modes in which we display a bunch of possible destinations. @@ -131,6 +168,15 @@ pub enum Mode { InputSimple(InputSimple), } +impl Mode { + /// True if the mode requires a view refresh when left. + /// Most modes don't, since they don't display their content in the first window. + /// content. But `Mode::Preview` does, since it uses the main window. + pub fn refresh_required(&self) -> bool { + matches!(*self, Mode::Preview) + } +} + impl fmt::Display for Mode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { @@ -150,6 +196,8 @@ impl fmt::Display for Mode { Mode::InputSimple(InputSimple::Password(password_kind, _, _)) => { write!(f, "{password_kind}") } + Mode::InputSimple(InputSimple::Remote) => write!(f, "Remote: "), + Mode::InputCompleted(InputCompleted::Exec) => write!(f, "Exec: "), Mode::InputCompleted(InputCompleted::Goto) => write!(f, "Goto : "), Mode::InputCompleted(InputCompleted::Search) => write!(f, "Search: "), diff --git a/src/opener.rs b/src/opener.rs index fe18f4a2..fe02fbae 100644 --- a/src/opener.rs +++ b/src/opener.rs @@ -16,6 +16,7 @@ use crate::constant_strings_paths::{ }; use crate::decompress::{decompress_gz, decompress_xz, decompress_zip}; use crate::fileinfo::extract_extension; +use crate::log::write_log_line; fn find_it

(exe_name: P) -> Option where @@ -392,7 +393,9 @@ pub fn execute_in_child + fmt::Debug>( exe: S, args: &[&str], ) -> Result { - info!("execute_in_child. executable: {exe:?}, arguments: {args:?}",); + info!("execute_in_child. executable: {exe:?}, arguments: {args:?}"); + let log_line = format!("Execute: {exe:?}, arguments: {args:?}"); + write_log_line(log_line); Ok(Command::new(exe).args(args).spawn()?) } @@ -452,7 +455,7 @@ pub fn execute_and_capture_output + fmt::Debug>( Ok(String::from_utf8(output.stdout)?) } else { Err(anyhow!( - "execute_and_capture_output: command didn't finished correctly", + "execute_and_capture_output: command didn't finish properly", )) } } diff --git a/src/password.rs b/src/password.rs index 49d98c11..1cd01583 100644 --- a/src/password.rs +++ b/src/password.rs @@ -4,6 +4,7 @@ use std::process::{Command, Stdio}; use anyhow::{Context, Result}; use log::info; +use crate::log::write_log_line; use crate::utils::current_username; /// Different kind of password @@ -84,38 +85,54 @@ impl PasswordHolder { } } -/// run a sudo command requiring a password (generally to establish the password.) -/// Since I can't send 2 passwords at a time, it will only work with the sudo password -/// It requires a path to establish CWD. -pub fn execute_sudo_command_with_password( - args: &[S], - password: &str, - path: P, -) -> Result<(bool, String, String)> +/// Spawn a sudo command with stdin, stdout and stderr piped. +/// sudo is run with -S argument to read the passworo from stdin +/// Args are sent. +/// CWD is set to `path`. +/// No password is set yet. +/// A password should be sent with `inject_password`. +fn new_sudo_command_awaiting_password(args: &[S], path: P) -> Result where S: AsRef + std::fmt::Debug, P: AsRef + std::fmt::Debug, { - info!("sudo_with_password {args:?} CWD {path:?}"); - info!( - target: "special", - "running sudo command with passwod. args: {args:?}, CWD: {path:?}" - ); - let mut child = Command::new("sudo") + Ok(Command::new("sudo") .arg("-S") .args(args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .current_dir(path) - .spawn()?; + .spawn()?) +} +/// Send password to a sudo command through its stdin. +fn inject_password(password: &str, child: &mut std::process::Child) -> Result<()> { let child_stdin = child .stdin .as_mut() .context("run_privileged_command: couldn't open child stdin")?; child_stdin.write_all(format!("{password}\n").as_bytes())?; + Ok(()) +} +/// run a sudo command requiring a password (generally to establish the password.) +/// Since I can't send 2 passwords at a time, it will only work with the sudo password +/// It requires a path to establish CWD. +pub fn execute_sudo_command_with_password( + args: &[S], + password: &str, + path: P, +) -> Result<(bool, String, String)> +where + S: AsRef + std::fmt::Debug, + P: AsRef + std::fmt::Debug, +{ + info!("sudo_with_password {args:?} CWD {path:?}"); + let log_line = format!("running sudo command with password. args: {args:?}, CWD: {path:?}"); + write_log_line(log_line); + let mut child = new_sudo_command_awaiting_password(args, path)?; + inject_password(password, &mut child)?; let output = child.wait_with_output()?; Ok(( output.status.success(), @@ -124,20 +141,30 @@ where )) } -/// Runs a passwordless sudo command. -/// Returns stdout & stderr -pub fn execute_sudo_command(args: &[S]) -> Result<(bool, String, String)> +/// Spawn a sudo command which shouldn't require a password. +/// The command is executed immediatly and we return an handle to it. +fn new_sudo_command_passwordless(args: &[S]) -> Result where S: AsRef + std::fmt::Debug, { - info!("running sudo {:?}", args); - info!(target: "special", "running sudo command. {args:?}"); - let child = Command::new("sudo") + Ok(Command::new("sudo") .args(args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) - .spawn()?; + .spawn()?) +} + +/// Runs a passwordless sudo command. +/// Returns stdout & stderr +pub fn execute_sudo_command(args: &[S]) -> Result<(bool, String, String)> +where + S: AsRef + std::fmt::Debug, +{ + info!("running sudo {:?}", args); + let log_line = format!("running sudo command. {args:?}"); + write_log_line(log_line); + let child = new_sudo_command_passwordless(args)?; let output = child.wait_with_output()?; Ok(( output.status.success(), diff --git a/src/preview.rs b/src/preview.rs index 8d686a65..be5f18b4 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -19,7 +19,10 @@ use tuikit::attr::{Attr, Color}; use users::UsersCache; use crate::config::Colors; -use crate::constant_strings_paths::THUMBNAIL_PATH; +use crate::constant_strings_paths::{ + DIFF, FFMPEG, FONTIMAGE, ISOINFO, JUPYTER, LSBLK, LSOF, MEDIAINFO, PANDOC, RSVG_CONVERT, SS, + THUMBNAIL_PATH, UEBERZUG, +}; use crate::content_window::ContentWindow; use crate::decompress::list_files_zip; use crate::fileinfo::{FileInfo, FileKind}; @@ -27,7 +30,7 @@ use crate::filter::FilterKind; use crate::opener::execute_and_capture_output_without_check; use crate::status::Status; use crate::tree::{ColoredString, Tree}; -use crate::utils::filename_from_path; +use crate::utils::{filename_from_path, is_program_in_path}; /// Different kind of preview used to display some informaitons /// About the file. @@ -45,6 +48,9 @@ pub enum Preview { Iso(Iso), Diff(Diff), ColoredText(ColoredText), + Socket(Socket), + BlockDevice(BlockDevice), + FifoCharDevice(FifoCharDevice), #[default] Empty, } @@ -53,6 +59,7 @@ pub enum Preview { pub enum TextKind { HELP, LOG, + EPUB, #[default] TEXTFILE, } @@ -82,32 +89,69 @@ impl Preview { FileKind::NormalFile => match file_info.extension.to_lowercase().as_str() { e if is_ext_compressed(e) => Ok(Self::Archive(ZipContent::new(&file_info.path)?)), e if is_ext_pdf(e) => Ok(Self::Pdf(PdfContent::new(&file_info.path))), - e if is_ext_image(e) => Ok(Self::Ueberzug(Ueberzug::image(&file_info.path)?)), - e if is_ext_audio(e) => Ok(Self::Media(MediaContent::new(&file_info.path)?)), - e if is_ext_video(e) => { + e if is_ext_image(e) && is_program_in_path(UEBERZUG) => { + Ok(Self::Ueberzug(Ueberzug::image(&file_info.path)?)) + } + e if is_ext_audio(e) && is_program_in_path(MEDIAINFO) => { + Ok(Self::Media(MediaContent::new(&file_info.path)?)) + } + e if is_ext_video(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(FFMPEG) => + { Ok(Self::Ueberzug(Ueberzug::video_thumbnail(&file_info.path)?)) } - e if is_ext_font(e) => { + e if is_ext_font(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(FONTIMAGE) => + { Ok(Self::Ueberzug(Ueberzug::font_thumbnail(&file_info.path)?)) } - e if is_ext_svg(e) => Ok(Self::Ueberzug(Ueberzug::svg_thumbnail(&file_info.path)?)), - e if is_ext_iso(e) => Ok(Self::Iso(Iso::new(&file_info.path)?)), - e if is_ext_notebook(e) => { + e if is_ext_svg(e) + && is_program_in_path(UEBERZUG) + && is_program_in_path(RSVG_CONVERT) => + { + Ok(Self::Ueberzug(Ueberzug::svg_thumbnail(&file_info.path)?)) + } + e if is_ext_iso(e) && is_program_in_path(ISOINFO) => { + Ok(Self::Iso(Iso::new(&file_info.path)?)) + } + e if is_ext_notebook(e) && is_program_in_path(JUPYTER) => { Ok(Self::notebook(&file_info.path) .context("Preview: Couldn't parse notebook")?) } - e if is_ext_doc(e) => { + e if is_ext_doc(e) && is_program_in_path(PANDOC) => { Ok(Self::doc(&file_info.path).context("Preview: Couldn't parse doc")?) } + e if is_ext_epub(e) && is_program_in_path(PANDOC) => { + Ok(Self::epub(&file_info.path).context("Preview: Couldn't parse epub")?) + } e => match Self::preview_syntaxed(e, &file_info.path) { Some(syntaxed_preview) => Ok(syntaxed_preview), None => Self::preview_text_or_binary(file_info), }, }, + FileKind::Socket if is_program_in_path(SS) => Ok(Self::socket(file_info)), + FileKind::BlockDevice if is_program_in_path(LSBLK) => Ok(Self::blockdevice(file_info)), + FileKind::Fifo | FileKind::CharDevice if is_program_in_path(LSOF) => { + Ok(Self::fifo_chardevice(file_info)) + } _ => Err(anyhow!("new preview: can't preview this filekind",)), } } + fn socket(file_info: &FileInfo) -> Self { + Self::Socket(Socket::new(file_info)) + } + + fn blockdevice(file_info: &FileInfo) -> Self { + Self::BlockDevice(BlockDevice::new(file_info)) + } + + fn fifo_chardevice(file_info: &FileInfo) -> Self { + Self::FifoCharDevice(FifoCharDevice::new(file_info)) + } + /// Creates a new, static window used when we display a preview in the second pane pub fn window_for_second_pane(&self, height: usize) -> ContentWindow { ContentWindow::new(self.len(), height) @@ -129,26 +173,28 @@ impl Preview { fn notebook(path: &Path) -> Option { let path_str = path.to_str()?; + // nbconvert is bundled with jupyter, no need to check again let output = execute_and_capture_output_without_check( - "jupyter", + JUPYTER, &["nbconvert", "--to", "markdown", path_str, "--stdout"], ) .ok()?; - let ss = SyntaxSet::load_defaults_nonewlines(); - ss.find_syntax_by_extension("md").map(|syntax| { - Self::Syntaxed(HLContent::from_str(&output, ss.clone(), syntax).unwrap_or_default()) - }) + Self::syntaxed_from_str(output, "md") } fn doc(path: &Path) -> Option { let path_str = path.to_str()?; let output = execute_and_capture_output_without_check( - "pandoc", + PANDOC, &["-s", "-t", "markdown", "--", path_str], ) .ok()?; + Self::syntaxed_from_str(output, "md") + } + + fn syntaxed_from_str(output: String, ext: &str) -> Option { let ss = SyntaxSet::load_defaults_nonewlines(); - ss.find_syntax_by_extension("md").map(|syntax| { + ss.find_syntax_by_extension(ext).map(|syntax| { Self::Syntaxed(HLContent::from_str(&output, ss.clone(), syntax).unwrap_or_default()) }) } @@ -164,7 +210,7 @@ impl Preview { } fn is_binary(file_info: &FileInfo, file: &mut std::fs::File, buffer: &mut [u8]) -> bool { - file_info.size().unwrap_or_default() >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64 + file_info.true_size >= Self::CONTENT_INSPECTOR_MIN_SIZE as u64 && file.read_exact(buffer).is_ok() && inspect(buffer) == ContentType::BINARY } @@ -191,6 +237,12 @@ impl Preview { Self::ColoredText(ColoredText::new(output)) } + pub fn epub(path: &Path) -> Result { + Ok(Self::Text( + TextContent::epub(path).context("Couldn't read epub")?, + )) + } + /// Empty preview, holding nothing. pub fn new_empty() -> Self { Self::Empty @@ -206,12 +258,15 @@ impl Preview { Self::Binary(binary) => binary.len(), Self::Pdf(pdf) => pdf.len(), Self::Archive(zip) => zip.len(), - Self::Ueberzug(_img) => 0, + Self::Ueberzug(_) => 0, Self::Media(media) => media.len(), Self::Directory(directory) => directory.len(), Self::Diff(diff) => diff.len(), Self::Iso(iso) => iso.len(), Self::ColoredText(text) => text.len(), + Self::Socket(socket) => socket.len(), + Self::BlockDevice(blockdevice) => blockdevice.len(), + Self::FifoCharDevice(fifo) => fifo.len(), } } @@ -221,6 +276,117 @@ impl Preview { } } +/// Read a number of lines from a text file. Returns a vector of strings. +fn read_nb_lines(path: &Path, size_limit: usize) -> Result> { + let reader = std::io::BufReader::new(std::fs::File::open(path)?); + Ok(reader + .lines() + .take(size_limit) + .map(|line| line.unwrap_or_else(|_| "".to_owned())) + .collect()) +} + +/// Preview a socket file with `ss -lpmepiT` +#[derive(Clone, Default)] +pub struct Socket { + content: Vec, + length: usize, +} + +impl Socket { + /// New socket preview + /// See `man ss` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec; + if let Ok(output) = std::process::Command::new(SS).arg("-lpmepiT").output() { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s + .lines() + .filter(|l| l.contains(&fileinfo.filename)) + .map(|s| s.to_owned()) + .collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + +/// Preview a blockdevice file with lsblk +#[derive(Clone, Default)] +pub struct BlockDevice { + content: Vec, + length: usize, +} + +impl BlockDevice { + /// New socket preview + /// See `man ss` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec; + if let Ok(output) = std::process::Command::new(LSBLK) + .args([ + "-lfo", + "FSTYPE,PATH,LABEL,UUID,FSVER,MOUNTPOINT,MODEL,SIZE,FSAVAIL,FSUSE%", + &fileinfo.path.display().to_string(), + ]) + .output() + { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s.lines().map(|s| s.to_owned()).collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + +/// Preview a fifo or a chardevice file with lsof +#[derive(Clone, Default)] +pub struct FifoCharDevice { + content: Vec, + length: usize, +} + +impl FifoCharDevice { + /// New FIFO preview + /// See `man lsof` for a description of the arguments. + fn new(fileinfo: &FileInfo) -> Self { + let content: Vec; + if let Ok(output) = std::process::Command::new(LSOF) + .arg(&fileinfo.path.display().to_string()) + .output() + { + let s = String::from_utf8(output.stdout).unwrap_or_default(); + content = s.lines().map(|s| s.to_owned()).collect(); + } else { + content = vec![]; + } + Self { + length: content.len(), + content, + } + } + + fn len(&self) -> usize { + self.length + } +} + /// Holds a preview of a text content. /// It's a boxed vector of strings (per line) #[derive(Clone, Default)] @@ -234,7 +400,7 @@ impl TextContent { const SIZE_LIMIT: usize = 1048576; fn help(help: &str) -> Self { - let content: Vec = help.split('\n').map(|s| s.to_owned()).collect(); + let content: Vec = help.lines().map(|line| line.to_owned()).collect(); Self { kind: TextKind::HELP, length: content.len(), @@ -250,13 +416,23 @@ impl TextContent { } } + fn epub(path: &Path) -> Option { + let path_str = path.to_str()?; + let output = execute_and_capture_output_without_check( + PANDOC, + &["-s", "-t", "plain", "--", path_str], + ) + .ok()?; + let content: Vec = output.lines().map(|line| line.to_owned()).collect(); + Some(Self { + kind: TextKind::EPUB, + length: content.len(), + content, + }) + } + fn from_file(path: &Path) -> Result { - let reader = std::io::BufReader::new(std::fs::File::open(path)?); - let content: Vec = reader - .lines() - .take(Self::SIZE_LIMIT) - .map(|line| line.unwrap_or_else(|_| "".to_owned())) - .collect(); + let content = read_nb_lines(path, Self::SIZE_LIMIT)?; Ok(Self { kind: TextKind::TEXTFILE, length: content.len(), @@ -279,18 +455,12 @@ pub struct HLContent { impl HLContent { const SIZE_LIMIT: usize = 32768; - /// Creates a new displayable content of a syntect supported file. /// It may file if the file isn't properly formatted or the extension /// is wrong (ie. python content with .c extension). /// ATM only Solarized (dark) theme is supported. fn new(path: &Path, syntax_set: SyntaxSet, syntax_ref: &SyntaxReference) -> Result { - let reader = std::io::BufReader::new(std::fs::File::open(path)?); - let raw_content: Vec = reader - .lines() - .take(Self::SIZE_LIMIT) - .map(|line| line.unwrap_or_else(|_| "".to_owned())) - .collect(); + let raw_content = read_nb_lines(path, Self::SIZE_LIMIT)?; let highlighted_content = Self::parse_raw_content(raw_content, syntax_set, syntax_ref)?; Ok(Self { @@ -406,7 +576,7 @@ impl BinaryContent { Ok(Self { path: file_info.path.clone(), - length: file_info.size().unwrap_or_default() / Self::LINE_WIDTH as u64, + length: file_info.true_size / Self::LINE_WIDTH as u64, content, }) } @@ -530,7 +700,7 @@ pub struct MediaContent { impl MediaContent { fn new(path: &Path) -> Result { let content: Vec; - if let Ok(output) = std::process::Command::new("mediainfo").arg(path).output() { + if let Ok(output) = std::process::Command::new(MEDIAINFO).arg(path).output() { let s = String::from_utf8(output.stdout).unwrap_or_default(); content = s.lines().map(|s| s.to_owned()).collect(); } else { @@ -612,7 +782,7 @@ impl Ueberzug { .to_str() .context("make_thumbnail: couldn't parse the path into a string")?; Self::make_thumbnail( - "ffmpeg", + FFMPEG, &[ "-i", path_str, @@ -630,7 +800,7 @@ impl Ueberzug { let path_str = font_path .to_str() .context("make_thumbnail: couldn't parse the path into a string")?; - Self::make_thumbnail("fontimage", &["-o", THUMBNAIL_PATH, path_str]) + Self::make_thumbnail(FONTIMAGE, &["-o", THUMBNAIL_PATH, path_str]) } fn make_svg_thumbnail(svg_path: &Path) -> Result<()> { @@ -638,7 +808,7 @@ impl Ueberzug { .to_str() .context("make_thumbnail: couldn't parse the path into a string")?; Self::make_thumbnail( - "rsvg-convert", + RSVG_CONVERT, &["--keep-aspect-ratio", path_str, "-o", THUMBNAIL_PATH], ) } @@ -694,7 +864,7 @@ impl ColoredText { /// if the directory has a lot of children. #[derive(Clone, Debug)] pub struct Directory { - pub content: Vec<(String, ColoredString)>, + pub content: Vec, pub tree: Tree, len: usize, pub selected_index: usize, @@ -777,19 +947,25 @@ impl Directory { /// Select the "next" element of the tree if any. /// This is the element immediatly below the current one. pub fn select_next(&mut self, colors: &Colors) -> Result<()> { - if self.selected_index + 1 < self.content.len() { + if self.selected_index < self.content.len() { + self.tree.increase_required_height(); + self.unselect_children(); self.selected_index += 1; + self.update_tree_position_from_index(colors)?; } - self.update_tree_position_from_index(colors) + Ok(()) } /// Select the previous sibling if any. /// This is the element immediatly below the current one. pub fn select_prev(&mut self, colors: &Colors) -> Result<()> { if self.selected_index > 0 { + self.tree.decrease_required_height(); + self.unselect_children(); self.selected_index -= 1; + self.update_tree_position_from_index(colors)?; } - self.update_tree_position_from_index(colors) + Ok(()) } /// Move up 10 times. @@ -805,8 +981,12 @@ impl Directory { /// Move down 10 times pub fn page_down(&mut self, colors: &Colors) -> Result<()> { self.selected_index += 10; - if self.selected_index >= self.content.len() { - self.selected_index = self.content.len() - 1; + if self.selected_index > self.content.len() { + if !self.content.is_empty() { + self.selected_index = self.content.len(); + } else { + self.selected_index = 1; + } } self.update_tree_position_from_index(colors) } @@ -848,18 +1028,19 @@ impl Directory { /// Calculates the top, bottom and lenght of the view, depending on which element /// is selected and the size of the window used to display. - pub fn calculate_tree_window(&self, height: usize) -> (usize, usize, usize) { + pub fn calculate_tree_window(&self, terminal_height: usize) -> (usize, usize, usize) { let length = self.content.len(); let top: usize; let bottom: usize; - if self.selected_index < height - 1 { + let window_height = terminal_height - ContentWindow::WINDOW_MARGIN_TOP; + if self.selected_index < terminal_height - 1 { top = 0; - bottom = height - 1; + bottom = window_height; } else { - let padding = std::cmp::max(10, height / 2); - top = self.selected_index - 1 - padding; - bottom = self.selected_index + height - 1 + padding; + let padding = std::cmp::max(10, terminal_height / 2); + top = self.selected_index - padding; + bottom = top + window_height; } (top, bottom, length) @@ -874,11 +1055,11 @@ pub struct Diff { impl Diff { pub fn new(first_path: &str, second_path: &str) -> Result { let content: Vec = - execute_and_capture_output_without_check("diff", &[first_path, second_path])? + execute_and_capture_output_without_check(DIFF, &[first_path, second_path])? .lines() .map(|s| s.to_owned()) .collect(); - info!("diff:\n{content:?}"); + info!("{DIFF}:\n{content:?}"); Ok(Self { length: content.len(), @@ -900,11 +1081,11 @@ impl Iso { fn new(path: &Path) -> Result { let path = path.to_str().context("couldn't parse the path")?; let content: Vec = - execute_and_capture_output_without_check("isoinfo", &["-l", "-i", path])? + execute_and_capture_output_without_check(ISOINFO, &["-l", "-i", path])? .lines() .map(|s| s.to_owned()) .collect(); - info!("isofino:\n{content:?}"); + info!("{ISOINFO}:\n{content:?}"); Ok(Self { length: content.len(), @@ -929,21 +1110,6 @@ pub trait Window { ) -> Take>>>; } -impl Window> for HLContent { - fn window( - &self, - top: usize, - bottom: usize, - length: usize, - ) -> std::iter::Take>>>> { - self.content - .iter() - .enumerate() - .skip(top) - .take(min(length, bottom + 1)) - } -} - macro_rules! impl_window { ($t:ident, $u:ident) => { impl Window<$u> for $t { @@ -963,17 +1129,26 @@ macro_rules! impl_window { }; } -type ColoredPair = (String, ColoredString); +/// A tuple with `(ColoredString, String, ColoredString)`. +/// Used to iter and impl window trait in tree mode. +pub type ColoredTriplet = (ColoredString, String, ColoredString); + +/// A vector of highlighted strings +pub type VecSyntaxedString = Vec; +impl_window!(HLContent, VecSyntaxedString); impl_window!(TextContent, String); impl_window!(BinaryContent, Line); impl_window!(PdfContent, String); impl_window!(ZipContent, String); impl_window!(MediaContent, String); -impl_window!(Directory, ColoredPair); +impl_window!(Directory, ColoredTriplet); impl_window!(Diff, String); impl_window!(Iso, String); impl_window!(ColoredText, String); +impl_window!(Socket, String); +impl_window!(BlockDevice, String); +impl_window!(FifoCharDevice, String); fn is_ext_compressed(ext: &str) -> bool { matches!( @@ -981,7 +1156,9 @@ fn is_ext_compressed(ext: &str) -> bool { "zip" | "gzip" | "bzip2" | "xz" | "lzip" | "lzma" | "tar" | "mtree" | "raw" | "7z" ) } -fn is_ext_image(ext: &str) -> bool { + +/// True iff the extension is a known (by me) image extension. +pub fn is_ext_image(ext: &str) -> bool { matches!( ext, "png" | "jpg" | "jpeg" | "tiff" | "heif" | "gif" | "raw" | "cr2" | "nef" | "orf" | "sr2" @@ -1011,11 +1188,11 @@ fn is_ext_video(ext: &str) -> bool { } fn is_ext_font(ext: &str) -> bool { - matches!(ext, "ttf") + matches!(ext, "ttf" | "otf") } fn is_ext_svg(ext: &str) -> bool { - matches!(ext, "svg") + matches!(ext, "svg" | "svgz") } fn is_ext_pdf(ext: &str) -> bool { @@ -1034,6 +1211,10 @@ fn is_ext_doc(ext: &str) -> bool { matches!(ext, "doc" | "docx" | "odt" | "sxw") } +fn is_ext_epub(ext: &str) -> bool { + ext == "epub" +} + fn catch_unwind_silent R + panic::UnwindSafe, R>(f: F) -> std::thread::Result { let prev_hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); diff --git a/src/shell_menu.rs b/src/shell_menu.rs index 4bb99324..fb54abd3 100644 --- a/src/shell_menu.rs +++ b/src/shell_menu.rs @@ -1,6 +1,7 @@ use anyhow::{Context, Result}; use crate::impl_selectable_content; +use crate::log::write_log_line; use crate::opener::{execute_in_child_without_output, execute_in_child_without_output_with_path}; use crate::status::Status; use crate::utils::is_program_in_path; @@ -22,13 +23,19 @@ impl Default for ShellMenu { impl ShellMenu { fn update_from_file(&mut self, yaml: &serde_yaml::mapping::Mapping) -> Result<()> { for (key, mapping) in yaml.into_iter() { - let Some(command) = key.as_str() else { continue; }; + let Some(command) = key.as_str() else { + continue; + }; if !is_program_in_path(command) { continue; } let command = command.to_owned(); - let Some(require_cwd) = mapping.get("cwd") else { continue; }; - let Some(require_cwd) = require_cwd.as_bool() else { continue; }; + let Some(require_cwd) = mapping.get("cwd") else { + continue; + }; + let Some(require_cwd) = require_cwd.as_bool() else { + continue; + }; self.content.push((command, require_cwd)); } Ok(()) @@ -43,6 +50,8 @@ impl ShellMenu { } else { Self::simple(status, name.as_str())? }; + let log_line = format!("Executed {name}"); + write_log_line(log_line); Ok(()) } diff --git a/src/shell_parser.rs b/src/shell_parser.rs index 53ffc9c1..f2dc3cd4 100644 --- a/src/shell_parser.rs +++ b/src/shell_parser.rs @@ -57,46 +57,65 @@ impl ShellCommandParser { match token { Token::Arg(string) => computed.push(string.to_owned()), Token::Selected => { - let path = status - .selected_non_mut() - .path_content - .selected_path_string() - .context("Empty directory")?; - computed.push(path); + computed.push(Self::selected(status)?); } Token::Path => { - let path = status - .selected_non_mut() - .path_content_str() - .context("Couldn't read path")? - .to_owned(); - computed.push(path); + computed.push(Self::path(status)?); } Token::Filename => { - let filename = status - .selected_non_mut() - .selected() - .context("Empty directory")? - .filename - .clone(); - computed.push(filename); + computed.push(Self::filename(status)?); } Token::Extension => { - let extension = status - .selected_non_mut() - .selected() - .context("Empty directory")? - .extension - .clone(); - computed.push(extension); - } - Token::Flagged => { - for path in status.flagged.content.iter() { - computed.push(path.to_str().context("Couldn't parse the path")?.to_owned()); - } + computed.push(Self::extension(status)?); } + Token::Flagged => computed.extend_from_slice(&Self::flagged(status)), } } Ok(computed) } + + fn selected(status: &Status) -> Result { + status + .selected_non_mut() + .path_content + .selected_path_string() + .context("Empty directory") + } + + fn path(status: &Status) -> Result { + Ok(status + .selected_non_mut() + .path_content_str() + .context("Couldn't read path")? + .to_owned()) + } + + fn filename(status: &Status) -> Result { + Ok(status + .selected_non_mut() + .selected() + .context("Empty directory")? + .filename + .clone()) + } + + fn extension(status: &Status) -> Result { + Ok(status + .selected_non_mut() + .selected() + .context("Empty directory")? + .extension + .clone()) + } + + fn flagged(status: &Status) -> Vec { + status + .flagged + .content + .iter() + .map(|path| path.to_str()) + .filter(|s| s.is_some()) + .map(|s| s.unwrap().to_owned()) + .collect() + } } diff --git a/src/shortcut.rs b/src/shortcut.rs index 3a494d11..e265e31b 100644 --- a/src/shortcut.rs +++ b/src/shortcut.rs @@ -77,6 +77,11 @@ impl Shortcut { shortcuts } + fn clear_doublons(&mut self) { + self.content.dedup(); + self.content = dedup_slow(self.content.clone()) + } + pub fn update_git_root(&mut self) { self.content[self.non_mount_size - 1] = Self::git_root_or_cwd(); } @@ -91,8 +96,30 @@ impl Shortcut { /// extend the vector with the mount points. pub fn refresh(&mut self, mount_points: &[&Path]) { self.content.truncate(self.non_mount_size); - self.extend_with_mount_points(mount_points) + self.extend_with_mount_points(mount_points); + self.clear_doublons(); + } +} + +/// Remove duplicates from a vector and returns it. +/// Elements should be `PartialEq`. +/// It removes element than are not consecutives and is very slow. +fn dedup_slow(mut elems: Vec) -> Vec +where + T: PartialEq, +{ + let mut to_remove = vec![]; + for i in 0..elems.len() { + for j in (i + 1)..elems.len() { + if elems[i] == elems[j] { + to_remove.push(j) + } + } + } + for i in to_remove.iter().rev() { + elems.remove(*i); } + elems } impl_selectable_content!(PathBuf, Shortcut); diff --git a/src/sort.rs b/src/sort.rs index abcf0cc0..2245c9f4 100644 --- a/src/sort.rs +++ b/src/sort.rs @@ -117,7 +117,7 @@ impl SortKind { SortBy::Kind => Self::sort_by_key_hrtb(files, |f| &f.kind_format), SortBy::File => Self::sort_by_key_hrtb(files, |f| &f.filename), SortBy::Date => Self::sort_by_key_hrtb(files, |f| &f.system_time), - SortBy::Size => Self::sort_by_key_hrtb(files, |f| &f.file_size), + SortBy::Size => Self::sort_by_key_hrtb(files, |f| &f.true_size), SortBy::Exte => Self::sort_by_key_hrtb(files, |f| &f.extension), } } else { @@ -125,7 +125,7 @@ impl SortKind { SortBy::Kind => Self::reversed_sort_by_key_hrtb(files, |f| &f.kind_format), SortBy::File => Self::reversed_sort_by_key_hrtb(files, |f| &f.filename), SortBy::Date => Self::reversed_sort_by_key_hrtb(files, |f| &f.system_time), - SortBy::Size => Self::reversed_sort_by_key_hrtb(files, |f| &f.file_size), + SortBy::Size => Self::reversed_sort_by_key_hrtb(files, |f| &f.true_size), SortBy::Exte => Self::reversed_sort_by_key_hrtb(files, |f| &f.extension), } } @@ -138,7 +138,7 @@ impl SortKind { SortBy::Kind => Self::sort_by_key_hrtb(trees, |f| &f.file().kind_format), SortBy::File => Self::sort_by_key_hrtb(trees, |f| &f.file().filename), SortBy::Date => Self::sort_by_key_hrtb(trees, |f| &f.file().system_time), - SortBy::Size => Self::sort_by_key_hrtb(trees, |f| &f.file().file_size), + SortBy::Size => Self::sort_by_key_hrtb(trees, |f| &f.file().true_size), SortBy::Exte => Self::sort_by_key_hrtb(trees, |f| &f.file().extension), } } else { @@ -146,7 +146,7 @@ impl SortKind { SortBy::Kind => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().kind_format), SortBy::File => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().filename), SortBy::Date => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().system_time), - SortBy::Size => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().file_size), + SortBy::Size => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().true_size), SortBy::Exte => Self::reversed_sort_by_key_hrtb(trees, |f| &f.file().extension), } } diff --git a/src/status.rs b/src/status.rs index bfcfe8f9..a9272015 100644 --- a/src/status.rs +++ b/src/status.rs @@ -5,6 +5,7 @@ use std::path::Path; use std::sync::Arc; use anyhow::{anyhow, Context, Result}; +use clap::Parser; use log::info; use regex::Regex; use skim::SkimItem; @@ -18,11 +19,12 @@ use crate::bulkrename::Bulk; use crate::cli_info::CliInfo; use crate::compress::Compresser; use crate::config::{Colors, Settings}; -use crate::constant_strings_paths::TUIS_PATH; +use crate::constant_strings_paths::{NVIM, SS, TUIS_PATH}; use crate::copy_move::{copy_move, CopyMove}; use crate::cryptsetup::{BlockDeviceAction, CryptoDeviceOpener}; use crate::flagged::Flagged; use crate::iso::IsoDevice; +use crate::log::write_log_line; use crate::marks::Marks; use crate::mode::{InputSimple, Mode, NeedConfirmation}; use crate::mount_help::MountHelper; @@ -39,7 +41,7 @@ use crate::skim::Skimer; use crate::tab::Tab; use crate::term_manager::MIN_WIDTH_FOR_DUAL_PANE; use crate::trash::Trash; -use crate::utils::{current_username, disk_space, filename_from_path}; +use crate::utils::{current_username, disk_space, filename_from_path, is_program_in_path}; /// Holds every mutable parameter of the application itself, except for /// the "display" information. @@ -59,8 +61,6 @@ pub struct Status { pub flagged: Flagged, /// Marks allows you to jump to a save mark pub marks: Marks, - /// Colors for extension - // pub colors: ColorCache, /// terminal pub term: Arc, skimer: Skimer, @@ -102,15 +102,15 @@ impl Status { /// It requires most of the information (arguments, configuration, height /// of the terminal, the formated help string). pub fn new( - args: Args, height: usize, term: Arc, help: String, opener: Opener, settings: &Settings, ) -> Result { + let args = Args::parse(); let Ok(shell_menu) = load_shell_menu(TUIS_PATH) else { - eprintln!("Couldn't load the TUIs config file at {TUIS_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/tuis.yaml for an example"); + eprintln!("Couldn't load the TUIs config file at {TUIS_PATH}. See https://raw.githubusercontent.com/qkzk/fm/master/config_files/fm/tuis.yaml for an example"); info!("Couldn't read tuis file at {TUIS_PATH}. Exiting"); std::process::exit(1); }; @@ -124,7 +124,7 @@ impl Status { let force_clear = false; let bulk = Bulk::default(); let start_folder = std::fs::canonicalize(std::path::PathBuf::from(&args.path))?; - + let dual_pane = settings.dual && term.term_size()?.0 >= MIN_WIDTH_FOR_DUAL_PANE; // unsafe because of UsersCache::with_all_users let users_cache = unsafe { UsersCache::with_all_users() }; let mut right_tab = Tab::new(args.clone(), height, users_cache)?; @@ -149,7 +149,7 @@ impl Status { marks: Marks::read_from_config_file(), skimer: Skimer::new(term.clone()), term, - dual_pane: settings.dual, + dual_pane, preview_second: false, system_info: sys, display_full: settings.full, @@ -217,7 +217,9 @@ impl Status { .to_str() .context("skim error")?, ); - let Some(output) = skim.first() else {return Ok(())}; + let Some(output) = skim.first() else { + return Ok(()); + }; self._update_tab_from_skim_output(output) } @@ -226,7 +228,9 @@ impl Status { /// The output is splited at `:` since we only care about the path, not the line number. pub fn skim_line_output_to_tab(&mut self) -> Result<()> { let skim = self.skimer.search_line_in_file(); - let Some(output) = skim.first() else {return Ok(())}; + let Some(output) = skim.first() else { + return Ok(()); + }; self._update_tab_from_skim_line_output(output) } @@ -235,11 +239,19 @@ impl Status { /// If the result can't be parsed, nothing is done. pub fn skim_find_keybinding(&mut self) -> Result<()> { let skim = self.skimer.search_in_text(self.help.clone()); - let Some(output) = skim.first() else { return Ok(()) }; + let Some(output) = skim.first() else { + return Ok(()); + }; let line = output.output().into_owned(); - let Some(keybind) = line.split(':').next() else { return Ok(()) }; - let Some(keyname) = parse_keyname(keybind) else { return Ok(()) }; - let Some(key) = from_keyname(&keyname) else { return Ok(()) }; + let Some(keybind) = line.split(':').next() else { + return Ok(()); + }; + let Some(keyname) = parse_keyname(keybind) else { + return Ok(()); + }; + let Some(key) = from_keyname(&keyname) else { + return Ok(()); + }; let event = Event::Key(key); let _ = self.term.borrow_mut().send_event(event); Ok(()) @@ -247,7 +259,9 @@ impl Status { fn _update_tab_from_skim_line_output(&mut self, skim_output: &Arc) -> Result<()> { let output_str = skim_output.output().to_string(); - let Some(filename) = output_str.split(':').next() else { return Ok(());}; + let Some(filename) = output_str.split(':').next() else { + return Ok(()); + }; let path = fs::canonicalize(filename)?; self._replace_path_by_skim_output(path) } @@ -260,7 +274,9 @@ impl Status { fn _replace_path_by_skim_output(&mut self, path: std::path::PathBuf) -> Result<()> { let tab = self.selected(); if path.is_file() { - let Some(parent) = path.parent() else { return Ok(()) }; + let Some(parent) = path.parent() else { + return Ok(()); + }; tab.set_pathcontent(parent)?; let filename = filename_from_path(&path)?; tab.search_from(filename, 0); @@ -301,6 +317,17 @@ impl Status { self.reset_tabs_view() } + pub fn click( + &mut self, + row: u16, + col: u16, + current_height: usize, + colors: &Colors, + ) -> Result<()> { + self.select_pane(col)?; + self.selected().select_row(row, colors, current_height) + } + /// Set the permissions of the flagged files according to a given permission. /// If the permission are invalid or if the user can't edit them, it may fail. pub fn set_permissions

(path: P, permissions: u32) -> Result<()> @@ -473,11 +500,9 @@ impl Status { } else { if iso_device.mount(¤t_username()?, &mut self.password_holder)? { info!("iso mounter mounted {iso_device:?}"); - info!( - target: "special", - "iso :\n{}", - iso_device.as_string()?, - ); + + let log_line = format!("iso : {}", iso_device.as_string()?,); + write_log_line(log_line); let path = iso_device.mountpoints.clone().context("no mount point")?; self.selected().set_pathcontent(&path)?; }; @@ -530,9 +555,13 @@ impl Status { } /// Move to the selected crypted device mount point. - pub fn move_to_encrypted_drive(&mut self) -> Result<()> { - let Some(device) = self.encrypted_devices.selected() else { return Ok(()) }; - let Some(mount_point) = device.mount_point() else { return Ok(())}; + pub fn go_to_encrypted_drive(&mut self) -> Result<()> { + let Some(device) = self.encrypted_devices.selected() else { + return Ok(()); + }; + let Some(mount_point) = device.mount_point() else { + return Ok(()); + }; let tab = self.selected(); let path = std::path::PathBuf::from(mount_point); tab.history.push(&path); @@ -576,7 +605,7 @@ impl Status { /// Execute a new mark, saving it to a config file for futher use. pub fn marks_new(&mut self, c: char, colors: &Colors) -> Result<()> { let path = self.selected().path_content.path.clone(); - self.marks.new_mark(c, path)?; + self.marks.new_mark(c, &path)?; { let tab: &mut Tab = self.selected(); tab.refresh_view() @@ -612,15 +641,17 @@ impl Status { /// isn't sufficiant to display enough information. /// We also need to know the new height of the terminal to start scrolling /// up or down. - pub fn resize(&mut self, width: usize, height: usize, colors: &Colors) -> Result<()> { + pub fn resize(&mut self, width: usize, height: usize) -> Result<()> { self.set_dual_pane_if_wide_enough(width)?; self.selected().set_height(height); - self.refresh_status(colors)?; + self.force_clear(); + self.refresh_users()?; Ok(()) } /// Recursively delete all flagged files. pub fn confirm_delete_files(&mut self, colors: &Colors) -> Result<()> { + let nb = self.flagged.len(); for pathbuf in self.flagged.content.iter() { if pathbuf.is_dir() { std::fs::remove_dir_all(pathbuf)?; @@ -628,6 +659,8 @@ impl Status { std::fs::remove_file(pathbuf)?; } } + let log_line = format!("Deleted {nb} flagged files"); + write_log_line(log_line); self.selected().reset_mode(); self.clear_flags_and_reset_view()?; self.refresh_status(colors) @@ -644,7 +677,9 @@ impl Status { fn run_sudo_command(&mut self, colors: &Colors) -> Result<()> { self.selected().set_mode(Mode::Normal); reset_sudo_faillock()?; - let Some(sudo_command) = &self.sudo_command else { return Ok(()); }; + let Some(sudo_command) = &self.sudo_command else { + return Ok(()); + }; let args = ShellCommandParser::new(sudo_command).compute(self)?; if args.is_empty() { return Ok(()); @@ -684,18 +719,46 @@ impl Status { if !self.nvim_server.is_empty() { return; } - let Ok(nvim_listen_address) = std::env::var("NVIM_LISTEN_ADDRESS") else { return; }; - self.nvim_server = nvim_listen_address; + if let Ok(nvim_listen_address) = std::env::var("NVIM_LISTEN_ADDRESS") { + self.nvim_server = nvim_listen_address; + return; + }; + if let Ok(nvim_listen_address) = Self::parse_nvim_address_from_ss_output() { + self.nvim_server = nvim_listen_address; + } + } + + fn parse_nvim_address_from_ss_output() -> Result { + if !is_program_in_path(SS) { + return Err(anyhow!("{SS} isn't installed")); + } + if let Ok(output) = std::process::Command::new(SS).arg("-l").output() { + let output = String::from_utf8(output.stdout).unwrap_or_default(); + let content: String = output + .split(&['\n', '\t', ' ']) + .filter(|w| w.contains(NVIM)) + .collect(); + if !content.is_empty() { + return Ok(content); + } + } + Err(anyhow!("Couldn't get nvim listen address from `ss` output")) } /// Execute a command requiring a confirmation (Delete, Move or Copy). - pub fn confirm_action( + /// The action is only executed if the user typed the char `y` + pub fn confirm( &mut self, + c: char, confirmed_action: NeedConfirmation, colors: &Colors, ) -> Result<()> { - self.match_confirmed_mode(confirmed_action, colors)?; - self.selected().reset_mode(); + if c == 'y' { + let _ = self.match_confirmed_mode(confirmed_action, colors); + } + if self.selected().reset_mode() { + self.selected().refresh_view()?; + } Ok(()) } @@ -715,21 +778,29 @@ impl Status { /// Select the left or right tab depending on where the user clicked. pub fn select_pane(&mut self, col: u16) -> Result<()> { let (width, _) = self.term_size()?; - if (col as usize) < width / 2 { - self.select_tab(0)?; + if self.dual_pane { + if (col as usize) < width / 2 { + self.select_tab(0)?; + } else { + self.select_tab(1)?; + }; } else { - self.select_tab(1)?; - }; + self.select_tab(0)?; + } Ok(()) } } fn parse_keyname(keyname: &str) -> Option { let mut split = keyname.split('('); - let Some(mutator) = split.next() else { return None; }; + let Some(mutator) = split.next() else { + return None; + }; let mut mutator = mutator.to_lowercase(); - let Some(param) = split.next() else { return Some(mutator) }; - let mut param = param.to_owned(); + let Some(param) = split.next() else { + return Some(mutator); + }; + let mut param = param.trim().to_owned(); mutator = mutator.replace("char", ""); param = param.replace([')', '\''], ""); if param.chars().all(char::is_uppercase) { diff --git a/src/tab.rs b/src/tab.rs index 1af24bf8..fe3c8a69 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -16,7 +16,7 @@ use crate::opener::execute_in_child; use crate::preview::{Directory, Preview}; use crate::selectable_content::SelectableContent; use crate::shortcut::Shortcut; -use crate::utils::{row_to_index, set_clipboard}; +use crate::utils::{row_to_window_index, set_clipboard}; use crate::visited::History; /// Holds every thing about the current tab of the application. @@ -97,7 +97,6 @@ impl Tab { /// Fill the input string with the currently selected completion. pub fn fill_completion(&mut self) -> Result<()> { - // self.completion.set_kind(&self.mode); match self.mode { Mode::InputCompleted(InputCompleted::Goto) => { let current_path = self.path_content_str().unwrap_or_default().to_owned(); @@ -125,19 +124,25 @@ impl Tab { } } + /// Refresh everything but the view + pub fn refresh_params(&mut self) -> Result<()> { + self.filter = FilterKind::All; + self.input.reset(); + self.preview = Preview::new_empty(); + self.completion.reset(); + self.directory.clear(); + Ok(()) + } + /// Refresh the current view. /// Input string is emptied, the files are read again, the window of /// displayed files is reset. /// The first file is selected. pub fn refresh_view(&mut self) -> Result<()> { - self.filter = FilterKind::All; - self.input.reset(); + self.refresh_params()?; self.path_content .reset_files(&self.filter, self.show_hidden)?; self.window.reset(self.path_content.content.len()); - self.preview = Preview::new_empty(); - self.completion.reset(); - self.directory.clear(); Ok(()) } @@ -217,8 +222,11 @@ impl Tab { /// Refresh the existing users. pub fn refresh_users(&mut self, users_cache: UsersCache) -> Result<()> { + let last_pathcontent_index = self.path_content.index; self.path_content - .refresh_users(users_cache, &self.filter, self.show_hidden) + .refresh_users(users_cache, &self.filter, self.show_hidden)?; + self.path_content.select_index(last_pathcontent_index); + Ok(()) } /// Search in current directory for an file whose name contains `searched_name`, @@ -263,7 +271,9 @@ impl Tab { /// Move to the parent of current path pub fn move_to_parent(&mut self) -> Result<()> { let path = self.path_content.path.clone(); - let Some(parent) = path.parent() else { return Ok(()) }; + let Some(parent) = path.parent() else { + return Ok(()); + }; self.set_pathcontent(parent) } @@ -294,14 +304,11 @@ impl Tab { /// Select the next sibling. pub fn tree_select_next(&mut self, colors: &Colors) -> Result<()> { - self.directory.tree.increase_required_height(); - self.directory.unselect_children(); self.directory.select_next(colors) } /// Select the previous siblging pub fn tree_select_prev(&mut self, colors: &Colors) -> Result<()> { - self.directory.unselect_children(); self.directory.select_prev(colors) } @@ -313,17 +320,28 @@ impl Tab { /// Go to the last leaf. pub fn tree_go_to_bottom_leaf(&mut self, colors: &Colors) -> Result<()> { - self.directory.tree.set_required_height(usize::MAX); + self.directory.tree.set_required_height_to_max(); self.directory.unselect_children(); self.directory.go_to_bottom_leaf(colors) } /// Returns the current path. - /// It Tree mode, it's the path of the selected node. - /// Else, it's the current path of pathcontent. - pub fn current_path(&mut self) -> &path::Path { + /// In tree mode : + /// if the selected node is a directory, that's it. + /// else, it is the parent of the selected node. + /// In other modes, it's the current path of pathcontent. + pub fn current_directory_path(&mut self) -> &path::Path { match self.mode { - Mode::Tree => &self.directory.tree.current_node.fileinfo.path, + Mode::Tree => { + let path = &self.directory.tree.current_node.fileinfo.path; + if path.is_dir() { + return path; + } + let Some(parent) = path.parent() else { + return path::Path::new("/"); + }; + parent + } _ => &self.path_content.path, } } @@ -380,9 +398,12 @@ impl Tab { /// Reset the last mode. /// The last mode is set to normal again. - pub fn reset_mode(&mut self) { + /// Returns True if the last mode requires a refresh afterwards. + pub fn reset_mode(&mut self) -> bool { + let must_refresh = self.mode.refresh_required(); self.mode = self.previous_mode; self.previous_mode = Mode::Normal; + must_refresh } /// Returns true if the current mode requires 2 windows. @@ -400,7 +421,7 @@ impl Tab { self.path_content.select_current(); self.window.scroll_down_one(self.path_content.index) } - Mode::Preview => self.window.scroll_down_one(self.window.bottom), + Mode::Preview => self.preview_page_down(), _ => (), } } @@ -414,7 +435,7 @@ impl Tab { self.path_content.select_current(); self.window.scroll_up_one(self.path_content.index) } - Mode::Preview => self.window.scroll_up_one(self.window.top), + Mode::Preview => self.preview_page_up(), _ => (), } } @@ -446,19 +467,16 @@ impl Tab { /// Select the first child of the current node and reset the display. pub fn select_first_child(&mut self, colors: &Colors) -> Result<()> { - self.directory.tree.increase_required_height(); self.tree_select_first_child(colors) } /// Select the next sibling of the current node. pub fn select_next(&mut self, colors: &Colors) -> Result<()> { - self.directory.tree.increase_required_height(); self.tree_select_next(colors) } /// Select the previous sibling of the current node. pub fn select_prev(&mut self, colors: &Colors) -> Result<()> { - self.directory.tree.decrease_required_height(); self.tree_select_prev(colors) } @@ -511,62 +529,77 @@ impl Tab { self.path_content.select_index(up_index); self.window.scroll_to(up_index) } - Mode::Preview => { - if self.window.top > 0 { - let skip = min(self.window.top, 30); - self.window.bottom -= skip; - self.window.top -= skip; - } - } + Mode::Preview => self.preview_page_up(), _ => (), } } + fn preview_page_up(&mut self) { + if self.window.top > 0 { + let skip = min(self.window.top, 30); + self.window.bottom -= skip; + self.window.top -= skip; + } + } + /// Move down 10 rows in normal mode. /// In other modes where vertical scrolling is possible (atm Preview), /// if moves down one page. pub fn page_down(&mut self) { match self.mode { - Mode::Normal => { - let down_index = min( - self.path_content.content.len() - 1, - self.path_content.index + 10, - ); - self.path_content.select_index(down_index); - self.window.scroll_to(down_index); - } - Mode::Preview => { - if self.window.bottom < self.preview.len() { - let skip = min(self.preview.len() - self.window.bottom, 30); - self.window.bottom += skip; - self.window.top += skip; - } - } + Mode::Normal => self.normal_page_down(), + Mode::Preview => self.preview_page_down(), _ => (), } } + fn normal_page_down(&mut self) { + let down_index = min( + self.path_content.content.len() - 1, + self.path_content.index + 10, + ); + self.path_content.select_index(down_index); + self.window.scroll_to(down_index); + } + + fn preview_page_down(&mut self) { + if self.window.bottom < self.preview.len() { + let skip = min(self.preview.len() - self.window.bottom, 30); + self.window.bottom += skip; + self.window.top += skip; + } + } + /// Select a given row, if there's something in it. - pub fn select_row(&mut self, row: u16, colors: &Colors) -> Result<()> { + pub fn select_row(&mut self, row: u16, colors: &Colors, term_height: usize) -> Result<()> { match self.mode { - Mode::Normal => { - let index = row_to_index(row); - self.path_content.select_index(index); - self.window.scroll_to(index); - } - Mode::Tree => { - let index = row_to_index(row) + 1; - self.directory.tree.unselect_children(); - self.directory.tree.position = self.directory.tree.position_from_index(index); - let (_, _, node) = self.directory.tree.select_from_position()?; - self.directory.make_preview(colors); - self.directory.tree.current_node = node; - } + Mode::Normal => self.normal_select_row(row), + Mode::Tree => self.tree_select_row(row, colors, term_height)?, _ => (), } Ok(()) } + fn normal_select_row(&mut self, row: u16) { + let screen_index = row_to_window_index(row); + let index = screen_index + self.window.top; + self.path_content.select_index(index); + self.window.scroll_to(index); + } + + fn tree_select_row(&mut self, row: u16, colors: &Colors, term_height: usize) -> Result<()> { + let screen_index = row_to_window_index(row) + 1; + // term.height = canvas.height + 2 rows for the canvas border + let (top, _, _) = self.directory.calculate_tree_window(term_height - 2); + let index = screen_index + top; + self.directory.tree.unselect_children(); + self.directory.tree.position = self.directory.tree.position_from_index(index); + let (_, _, node) = self.directory.tree.select_from_position()?; + self.directory.make_preview(colors); + self.directory.tree.current_node = node; + Ok(()) + } + /// Sort the file with given criteria /// Valid kind of sorts are : /// by kind : directory first, files next, in alphanumeric order diff --git a/src/term_manager.rs b/src/term_manager.rs index 330906d0..cc225ef7 100644 --- a/src/term_manager.rs +++ b/src/term_manager.rs @@ -13,12 +13,11 @@ use crate::completion::InputCompleted; use crate::compress::CompressionMethod; use crate::config::Colors; use crate::constant_strings_paths::{ - CHMOD_LINES, FILTER_LINES, HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE, LOG_FIRST_SENTENCE, - LOG_SECOND_SENTENCE, NEWDIR_LINES, NEWFILE_LINES, NVIM_ADDRESS_LINES, PASSWORD_LINES, - REGEX_LINES, RENAME_LINES, SHELL_LINES, SORT_LINES, + HELP_FIRST_SENTENCE, HELP_SECOND_SENTENCE, LOG_FIRST_SENTENCE, LOG_SECOND_SENTENCE, }; use crate::content_window::ContentWindow; use crate::fileinfo::{fileinfo_attr, shorten_path, FileInfo}; +use crate::log::read_last_log_line; use crate::mode::{InputSimple, MarkAction, Mode, Navigate, NeedConfirmation}; use crate::mount_help::MountHelper; use crate::preview::{Preview, TextKind, Window}; @@ -83,6 +82,11 @@ impl EventReader { pub fn poll_event(&self) -> Result { Ok(self.term.poll_event()?) } + + /// Height of the current terminal + pub fn term_height(&self) -> Result { + Ok(self.term.term_size()?.1) + } } macro_rules! impl_preview { @@ -94,19 +98,42 @@ macro_rules! impl_preview { }; } +/// Bunch of attributes describing the state of a main window +/// relatively to other windows +struct WinMainAttributes { + /// horizontal position, in cells + x_position: usize, + /// is this the first (false) or second (true) window ? + is_second: bool, + /// is this tab selected ? + is_selected: bool, + /// is there a secondary window ? + has_window_below: bool, +} + +impl WinMainAttributes { + fn new(x_position: usize, is_second: bool, is_selected: bool, has_window_below: bool) -> Self { + Self { + x_position, + is_second, + is_selected, + has_window_below, + } + } +} + struct WinMain<'a> { status: &'a Status, tab: &'a Tab, disk_space: &'a str, colors: &'a Colors, - x_position: usize, - is_second: bool, + attributes: WinMainAttributes, } impl<'a> Draw for WinMain<'a> { fn draw(&self, canvas: &mut dyn Canvas) -> DrawResult<()> { canvas.clear()?; - if self.status.dual_pane && self.is_second && self.status.preview_second { + if self.status.dual_pane && self.attributes.is_second && self.status.preview_second { self.preview_as_second_pane(canvas)?; return Ok(()); } @@ -134,16 +161,14 @@ impl<'a> WinMain<'a> { index: usize, disk_space: &'a str, colors: &'a Colors, - abs: usize, - is_second: bool, + attributes: WinMainAttributes, ) -> Self { Self { status, tab: &status.tabs[index], disk_space, colors, - x_position: abs, - is_second, + attributes, } } @@ -151,7 +176,7 @@ impl<'a> WinMain<'a> { let tab = &self.status.tabs[0]; let (_, height) = canvas.size()?; self.preview(tab, &tab.preview.window_for_second_pane(height), canvas)?; - draw_colored_strings(0, 0, self.default_preview_first_line(tab), canvas)?; + draw_colored_strings(0, 0, self.default_preview_first_line(tab), canvas, false)?; Ok(()) } @@ -161,24 +186,29 @@ impl<'a> WinMain<'a> { /// When a confirmation is needed we ask the user to input `'y'` or /// something else. /// Returns the result of the number of printed chars. + /// The colors are reversed when the tab is selected. It gives a visual indication of where he is. fn first_line(&self, tab: &Tab, disk_space: &str, canvas: &mut dyn Canvas) -> Result<()> { - draw_colored_strings(0, 0, self.create_first_row(tab, disk_space)?, canvas) + draw_colored_strings( + 0, + 0, + self.create_first_row(tab, disk_space)?, + canvas, + self.attributes.is_selected, + ) } fn second_line(&self, status: &Status, tab: &Tab, canvas: &mut dyn Canvas) -> Result { match tab.mode { - Mode::Normal => { + Mode::Normal | Mode::Tree => { if !status.display_full { - let Some(file) = tab.selected() else { return Ok(0) }; + let Some(file) = tab.selected() else { + return Ok(0); + }; self.second_line_detailed(file, canvas) } else { self.second_line_simple(status, canvas) } } - Mode::Tree => { - let Some(file) = tab.selected() else { return Ok(0) }; - self.second_line_detailed(file, canvas) - } _ => Ok(0), } } @@ -202,21 +232,52 @@ impl<'a> WinMain<'a> { fn normal_first_row(&self, disk_space: &str) -> Result> { Ok(vec![ - format!("{} ", shorten_path(&self.tab.path_content.path, None)?), - format!("{} files ", self.tab.path_content.true_len()), + format!(" {}", shorten_path(&self.tab.path_content.path, None)?), + self.first_row_filename(), + self.first_row_position(), format!("{} ", self.tab.path_content.used_space()), - format!("Avail: {disk_space} "), - format!("{} ", &self.tab.path_content.git_string()?), - format!("{} flags ", &self.status.flagged.len()), - format!("{}", &self.tab.path_content.sort_kind), + format!(" Avail: {disk_space} "), + format!(" {} ", self.tab.path_content.git_string()?), + self.first_row_flags(), + format!(" {} ", &self.tab.path_content.sort_kind), ]) } + fn first_row_filename(&self) -> String { + match self.tab.mode { + Mode::Tree => "".to_owned(), + _ => { + if let Some(fileinfo) = self.tab.path_content.selected() { + fileinfo.filename_without_dot_dotdot() + } else { + "".to_owned() + } + } + } + } + + fn first_row_position(&self) -> String { + format!( + " {} / {} ", + self.tab.path_content.index + 1, + self.tab.path_content.true_len() + 2 + ) + } + + fn first_row_flags(&self) -> String { + let nb_flagged = self.status.flagged.len(); + let flag_string = if self.status.flagged.len() > 1 { + "flags" + } else { + "flag" + }; + format!(" {nb_flagged} {flag_string} ",) + } + fn help_first_row() -> Vec { - let version = std::env!("CARGO_PKG_VERSION"); vec![ HELP_FIRST_SENTENCE.to_owned(), - format!("Version: {version} "), + format!(" Version: {v} ", v = std::env!("CARGO_PKG_VERSION")), HELP_SECOND_SENTENCE.to_owned(), ] } @@ -231,13 +292,11 @@ impl<'a> WinMain<'a> { fn default_preview_first_line(&self, tab: &Tab) -> Vec { match tab.path_content.selected() { Some(fileinfo) => { - let mut strings = vec![ - "Preview ".to_owned(), - format!("{}", fileinfo.path.to_string_lossy()), - ]; + let mut strings = vec![" Preview ".to_owned()]; if !tab.preview.is_empty() { - strings.push(format!(" {} / {}", tab.window.bottom, tab.preview.len())); + strings.push(format!(" {} / {} ", tab.window.bottom, tab.preview.len())); }; + strings.push(format!(" {} ", fileinfo.path.to_string_lossy())); strings } None => vec!["".to_owned()], @@ -287,7 +346,7 @@ impl<'a> WinMain<'a> { .content .iter() .enumerate() - .take(min(len, tab.window.bottom + 1)) + .take(min(len, tab.window.bottom)) .skip(tab.window.top) { let row = i + ContentWindow::WINDOW_MARGIN_TOP - tab.window.top; @@ -303,22 +362,41 @@ impl<'a> WinMain<'a> { canvas.print_with_attr(row, 0, &string, attr)?; } self.second_line(status, tab, canvas)?; + if !self.attributes.has_window_below { + self.log_line(canvas)?; + } Ok(()) } - fn tree(&self, status: &Status, tab: &Tab, canvas: &mut dyn Canvas) -> Result<()> { - let line_number_width = 3; + fn log_line(&self, canvas: &mut dyn Canvas) -> Result<()> { + let (_, height) = canvas.size()?; + canvas.print_with_attr(height - 1, 4, &read_last_log_line(), ATTR_YELLOW_BOLD)?; + Ok(()) + } + fn tree(&self, status: &Status, tab: &Tab, canvas: &mut dyn Canvas) -> Result<()> { + let left_margin = if status.display_full { 0 } else { 3 }; let (_, height) = canvas.size()?; let (top, bottom, len) = tab.directory.calculate_tree_window(height); - for (i, (prefix, colored_string)) in tab.directory.window(top, bottom, len) { + + for (i, (metadata, prefix, colored_string)) in tab.directory.window(top, bottom, len) { let row = i + ContentWindow::WINDOW_MARGIN_TOP - top; - let col = canvas.print(row, line_number_width, prefix)?; let mut attr = colored_string.attr; if status.flagged.contains(&colored_string.path) { attr.effect |= Effect::BOLD | Effect::UNDERLINE; } - canvas.print_with_attr(row, line_number_width + col + 1, &colored_string.text, attr)?; + let col_metadata = if status.display_full { + canvas.print_with_attr(row, left_margin, &metadata.text, attr)? + } else { + 0 + }; + let col_tree_prefix = canvas.print(row, left_margin + col_metadata, prefix)?; + canvas.print_with_attr( + row, + left_margin + col_metadata + col_tree_prefix + 1, + &colored_string.text, + attr, + )?; } self.second_line(status, tab, canvas)?; Ok(()) @@ -375,14 +453,14 @@ impl<'a> WinMain<'a> { Preview::Ueberzug(image) => { let (width, height) = canvas.size()?; image.ueberzug( - self.x_position as u16 + 2, + self.attributes.x_position as u16 + 2, 3, width as u16 - 2, height as u16 - 2, ); } Preview::Directory(directory) => { - for (i, (prefix, colored_string)) in + for (i, (_, prefix, colored_string)) in (directory).window(window.top, window.bottom, length) { let row = calc_line_row(i, window); @@ -422,6 +500,15 @@ impl<'a> WinMain<'a> { Preview::Iso(text) => { impl_preview!(text, tab, length, canvas, line_number_width, window) } + Preview::Socket(text) => { + impl_preview!(text, tab, length, canvas, line_number_width, window) + } + Preview::BlockDevice(text) => { + impl_preview!(text, tab, length, canvas, line_number_width, window) + } + Preview::FifoCharDevice(text) => { + impl_preview!(text, tab, length, canvas, line_number_width, window) + } Preview::Empty => (), } @@ -440,7 +527,7 @@ impl<'a> Draw for WinSecondary<'a> { Mode::Navigate(mode) => self.navigate(mode, canvas), Mode::NeedConfirmation(mode) => self.confirm(self.status, self.tab, mode, canvas), Mode::InputCompleted(_) => self.completion(self.tab, canvas), - Mode::InputSimple(mode) => Self::input_simple(mode, canvas), + Mode::InputSimple(mode) => Self::display_static_lines(mode.lines(), canvas), _ => Ok(()), }?; self.cursor(self.tab, canvas)?; @@ -463,7 +550,7 @@ impl<'a> WinSecondary<'a> { } fn first_line(&self, tab: &Tab, canvas: &mut dyn Canvas) -> Result<()> { - draw_colored_strings(0, 0, self.create_first_row(tab)?, canvas) + draw_colored_strings(0, 0, self.create_first_row(tab)?, canvas, false) } fn create_first_row(&self, tab: &Tab) -> Result> { @@ -482,7 +569,7 @@ impl<'a> WinSecondary<'a> { vec![format!("{password_kind}"), tab.input.password()] } Mode::InputCompleted(mode) => { - let mut completion_strings = vec![format!("{}", &tab.mode), tab.input.string()]; + let mut completion_strings = vec![tab.mode.to_string(), tab.input.string()]; if let Some(completion) = tab.completion.complete_input_string(&tab.input.string()) { completion_strings.push(completion.to_owned()) @@ -496,10 +583,7 @@ impl<'a> WinSecondary<'a> { completion_strings } _ => { - vec![ - format!("{}", tab.mode.clone()), - format!("{}", tab.input.string()), - ] + vec![tab.mode.to_string(), tab.input.string()] } }; Ok(first_row) @@ -519,25 +603,6 @@ impl<'a> WinSecondary<'a> { Ok(()) } - fn input_simple_lines(mode: InputSimple) -> &'static [&'static str] { - match mode { - InputSimple::Chmod => &CHMOD_LINES, - InputSimple::Filter => &FILTER_LINES, - InputSimple::Newdir => &NEWDIR_LINES, - InputSimple::Newfile => &NEWFILE_LINES, - InputSimple::Password(_, _, _) => &PASSWORD_LINES, - InputSimple::RegexMatch => ®EX_LINES, - InputSimple::Rename => &RENAME_LINES, - InputSimple::SetNvimAddr => &NVIM_ADDRESS_LINES, - InputSimple::Shell => &SHELL_LINES, - InputSimple::Sort => &SORT_LINES, - } - } - - fn input_simple(mode: InputSimple, canvas: &mut dyn Canvas) -> Result<()> { - Self::display_static_lines(Self::input_simple_lines(mode), canvas) - } - fn display_static_lines(lines: &[&str], canvas: &mut dyn Canvas) -> Result<()> { for (row, line, attr) in enumerated_colored_iter!(lines) { canvas.print_with_attr(row + ContentWindow::WINDOW_MARGIN_TOP, 4, line, *attr)?; @@ -548,11 +613,7 @@ impl<'a> WinSecondary<'a> { /// Display a cursor in the top row, at a correct column. fn cursor(&self, tab: &Tab, canvas: &mut dyn Canvas) -> Result<()> { match tab.mode { - Mode::Normal - | Mode::Tree - | Mode::Navigate(Navigate::Marks(_)) - | Mode::Navigate(_) - | Mode::Preview => { + Mode::Normal | Mode::Tree | Mode::Navigate(_) | Mode::Preview => { canvas.show_cursor(false)?; } Mode::InputSimple(InputSimple::Sort) => { @@ -641,17 +702,26 @@ impl<'a> WinSecondary<'a> { "Enter: restore the selected file - x: delete permanently", )?; let content = &selectable.content(); - for (row, trashinfo, attr) in enumerated_colored_iter!(content) { - let mut attr = *attr; - if row == selectable.index() { - attr.effect |= Effect::REVERSE; - } + if content.is_empty() { let _ = canvas.print_with_attr( - row + ContentWindow::WINDOW_MARGIN_TOP, + ContentWindow::WINDOW_MARGIN_TOP + 2, 4, - &format!("{trashinfo}"), - attr, + "Trash is empty", + ATTR_YELLOW_BOLD, ); + } else { + for (row, trashinfo, attr) in enumerated_colored_iter!(content) { + let mut attr = *attr; + if row == selectable.index() { + attr.effect |= Effect::REVERSE; + } + let _ = canvas.print_with_attr( + row + ContentWindow::WINDOW_MARGIN_TOP, + 4, + &format!("{trashinfo}"), + attr, + ); + } } Ok(()) } @@ -778,7 +848,7 @@ impl<'a> WinSecondary<'a> { )?; } } - NeedConfirmation::Copy | NeedConfirmation::Delete | NeedConfirmation::Move => { + _ => { for (row, path) in status.flagged.content.iter().enumerate() { canvas.print_with_attr( row + ContentWindow::WINDOW_MARGIN_TOP + 2, @@ -789,20 +859,12 @@ impl<'a> WinSecondary<'a> { } } } - let confirmation_string = match confirmed_mode { - NeedConfirmation::Copy => { - format!( - "Files will be copied to {}", - tab.path_content.path_to_str()? - ) - } - NeedConfirmation::Delete => "Files will deleted permanently".to_owned(), - NeedConfirmation::Move => { - format!("Files will be moved to {}", tab.path_content.path_to_str()?) - } - NeedConfirmation::EmptyTrash => "Trash will be emptied".to_owned(), - }; - canvas.print_with_attr(2, 3, &confirmation_string, ATTR_YELLOW_BOLD)?; + canvas.print_with_attr( + 2, + 3, + &confirmed_mode.confirmation_string(&tab.path_content.path_to_str()), + ATTR_YELLOW_BOLD, + )?; Ok(()) } @@ -927,8 +989,21 @@ impl Display { colors: &Colors, ) -> Result<()> { let (width, _) = self.term.term_size()?; - let win_main_left = WinMain::new(status, 0, disk_space_tab_0, colors, 0, false); - let win_main_right = WinMain::new(status, 1, disk_space_tab_1, colors, width / 2, true); + let (first_selected, second_selected) = (status.index == 0, status.index == 1); + let attributes_left = WinMainAttributes::new( + 0, + false, + first_selected, + status.tabs[0].need_second_window(), + ); + let win_main_left = WinMain::new(status, 0, disk_space_tab_0, colors, attributes_left); + let attributes_right = WinMainAttributes::new( + width / 2, + true, + second_selected, + status.tabs[1].need_second_window(), + ); + let win_main_right = WinMain::new(status, 1, disk_space_tab_1, colors, attributes_right); let win_second_left = WinSecondary::new(status, 0); let win_second_right = WinSecondary::new(status, 1); let (border_left, border_right) = self.borders(status); @@ -956,7 +1031,9 @@ impl Display { disk_space_tab_0: &str, colors: &Colors, ) -> Result<()> { - let win_main_left = WinMain::new(status, 0, disk_space_tab_0, colors, 0, false); + let attributes_left = + WinMainAttributes::new(0, false, true, status.tabs[0].need_second_window()); + let win_main_left = WinMain::new(status, 0, disk_space_tab_0, colors, attributes_left); let win_second_left = WinSecondary::new(status, 0); let percent_left = self.size_for_second_window(&status.tabs[0])?; let win = self.vertical_split( @@ -986,15 +1063,21 @@ const fn color_to_attr(color: Color) -> Attr { effect: Effect::empty(), } } + fn draw_colored_strings( row: usize, offset: usize, strings: Vec, canvas: &mut dyn Canvas, + reverse: bool, ) -> Result<()> { let mut col = 0; for (text, attr) in std::iter::zip(strings.iter(), FIRST_LINE_COLORS.iter().cycle()) { - col += canvas.print_with_attr(row, offset + col, text, *attr)?; + let mut attr = *attr; + if reverse { + attr.effect |= Effect::REVERSE; + } + col += canvas.print_with_attr(row, offset + col, text, attr)?; } Ok(()) } diff --git a/src/trash.rs b/src/trash.rs index 1bdf3929..75aa28db 100644 --- a/src/trash.rs +++ b/src/trash.rs @@ -10,6 +10,7 @@ use rand::{thread_rng, Rng}; use crate::constant_strings_paths::{TRASH_FOLDER_FILES, TRASH_FOLDER_INFO}; use crate::impl_selectable_content; +use crate::log::write_log_line; use crate::utils::read_lines; static TRASHINFO_DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S"; @@ -87,7 +88,9 @@ DeletionDate={} let dest_name = Self::remove_extension(dest_name.to_str().unwrap().to_owned())?; if let Ok(lines) = read_lines(trash_info_file) { for (index, line_result) in lines.enumerate() { - let Ok(line) = line_result.as_ref() else { continue }; + let Ok(line) = line_result.as_ref() else { + continue; + }; if line.starts_with("[Trash Info]") { if index == 0 { found_trash_info_line = true; @@ -254,7 +257,8 @@ impl Trash { std::fs::rename(origin, &trashfile_filename)?; info!("moved to trash {:?} -> {:?}", origin, dest_file_name); - info!(target:"special", "moved to trash {:?} -> {:?}", origin, dest_file_name); + let log_line = format!("moved to trash {:?} -> {:?}", origin, dest_file_name); + write_log_line(log_line); Ok(()) } @@ -271,10 +275,11 @@ impl Trash { self.content = vec![]; - info!(target: "special", + let log_line = format!( "Emptied the trash: {} files permanently deleted", number_of_elements ); + write_log_line(log_line); info!( "Emptied the trash: {} files permanently deleted", number_of_elements @@ -331,7 +336,8 @@ impl Trash { std::fs::create_dir_all(&parent)? } std::fs::rename(trashed_file_content, &origin)?; - info!(target: "special", "Trash restored: {origin}", origin=origin.display()); + let log_line = format!("Trash restored: {origin}", origin = origin.display()); + write_log_line(log_line); Ok(()) } @@ -347,11 +353,11 @@ impl Trash { if self.index > 0 { self.index -= 1 } - info!( - target: "special", + let log_line = format!( "Trash removed {trashed_file_content}", trashed_file_content = trashed_file_content.display() ); + write_log_line(log_line); Ok(()) } } diff --git a/src/tree.rs b/src/tree.rs index 3bdbb073..cc7605d9 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -7,6 +7,7 @@ use users::UsersCache; use crate::config::Colors; use crate::fileinfo::{fileinfo_attr, files_collection, FileInfo, FileKind}; use crate::filter::FilterKind; +use crate::preview::ColoredTriplet; use crate::sort::SortKind; use crate::utils::filename_from_path; @@ -38,6 +39,14 @@ impl ColoredString { }; Self::new(text, current_node.attr(colors), current_node.filepath()) } + + fn from_metadata_line(current_node: &Node, colors: &Colors) -> Self { + Self::new( + current_node.metadata_line.to_owned(), + current_node.attr(colors), + current_node.filepath(), + ) + } } /// An element in a tree. @@ -49,6 +58,7 @@ pub struct Node { pub position: Vec, pub folded: bool, pub is_dir: bool, + pub metadata_line: String, } impl Node { @@ -84,12 +94,24 @@ impl Node { self.folded = !self.folded; } - fn from_fileinfo(fileinfo: FileInfo, parent_position: Vec) -> Self { - Self { - is_dir: matches!(fileinfo.file_kind, FileKind::Directory), + fn from_fileinfo(fileinfo: FileInfo, parent_position: Vec) -> Result { + let is_dir = matches!(fileinfo.file_kind, FileKind::Directory); + Ok(Self { + is_dir, + metadata_line: fileinfo.format_no_filename()?, fileinfo, position: parent_position, folded: false, + }) + } + + fn empty(fileinfo: FileInfo) -> Self { + Self { + fileinfo, + position: vec![0], + folded: false, + is_dir: false, + metadata_line: "".to_owned(), } } } @@ -115,6 +137,11 @@ impl Tree { /// are present and the exploration is slow. pub const MAX_DEPTH: usize = 5; pub const REQUIRED_HEIGHT: usize = 80; + const MAX_INDEX: usize = 2 << 20; + + pub fn set_required_height_to_max(&mut self) { + self.set_required_height(Self::MAX_INDEX) + } /// Set the required height to a given value. /// The required height is used to stop filling the view content. @@ -124,13 +151,17 @@ impl Tree { /// The required height is used to stop filling the view content. pub fn increase_required_height(&mut self) { - self.required_height += 1; + if self.required_height < Self::MAX_INDEX { + self.required_height += 1; + } } /// Add 10 to the required height. /// The required height is used to stop filling the view content. pub fn increase_required_height_by_ten(&mut self) { - self.required_height += 10; + if self.required_height < Self::MAX_INDEX { + self.required_height += 10; + } } /// Reset the required height to its default value : Self::MAX_HEIGHT @@ -208,7 +239,7 @@ impl Tree { &sort_kind, parent_position.clone(), )?; - let node = Node::from_fileinfo(fileinfo, parent_position); + let node = Node::from_fileinfo(fileinfo, parent_position)?; let position = vec![0]; let current_node = node.clone(); Ok(Self { @@ -233,10 +264,14 @@ impl Tree { if max_depth == 0 { return Ok(vec![]); } - let FileKind::Directory = fileinfo.file_kind else { return Ok(vec![]) }; + let FileKind::Directory = fileinfo.file_kind else { + return Ok(vec![]); + }; let Some(mut files) = - files_collection(fileinfo, users_cache, display_hidden, filter_kind, true) - else { return Ok(vec![]) }; + files_collection(fileinfo, users_cache, display_hidden, filter_kind, true) + else { + return Ok(vec![]); + }; sort_kind.sort(&mut files); let leaves = files .iter() @@ -252,8 +287,8 @@ impl Tree { display_hidden, position, ) - .unwrap() }) + .filter_map(|r| r.ok()) .collect(); Ok(leaves) @@ -277,23 +312,19 @@ impl Tree { pub fn empty(path: &Path, users_cache: &UsersCache) -> Result { let filename = filename_from_path(path)?; let fileinfo = FileInfo::from_path_with_name(path, filename, users_cache)?; - let node = Node { - fileinfo, - position: vec![0], - folded: false, - is_dir: false, - }; + let node = Node::empty(fileinfo); let leaves = vec![]; let position = vec![0]; let current_node = node.clone(); let sort_kind = SortKind::tree_default(); + let required_height = 0; Ok(Self { node, leaves, position, current_node, sort_kind, - required_height: 0, + required_height, }) } @@ -410,7 +441,7 @@ impl Tree { /// We first create a position with max value (usize::MAX) and max size (Self::MAX_DEPTH). /// Then we select this node and adjust the position. pub fn go_to_bottom_leaf(&mut self) -> Result<()> { - self.position = vec![usize::MAX; Self::MAX_DEPTH]; + self.position = vec![Self::MAX_INDEX; Self::MAX_DEPTH]; let (depth, last_cord, node) = self.select_from_position()?; self.fix_position(depth, last_cord); self.current_node = node; @@ -433,10 +464,7 @@ impl Tree { /// is reached. There's no way atm to avoid parsing the first lines /// since the "prefix" (straight lines at left of screen) can reach /// the whole screen. - pub fn into_navigable_content( - &mut self, - colors: &Colors, - ) -> (usize, Vec<(String, ColoredString)>) { + pub fn into_navigable_content(&mut self, colors: &Colors) -> (usize, Vec) { let required_height = self.required_height; let mut stack = vec![("".to_owned(), self)]; let mut content = vec![]; @@ -448,6 +476,7 @@ impl Tree { } content.push(( + ColoredString::from_metadata_line(¤t.node, colors), prefix.to_owned(), ColoredString::from_node(¤t.node, colors), )); @@ -457,7 +486,9 @@ impl Tree { let other_prefix = other_prefix(prefix); let mut leaves = current.leaves.iter_mut(); - let Some(first_leaf) = leaves.next() else { continue; }; + let Some(first_leaf) = leaves.next() else { + continue; + }; stack.push((first_prefix.clone(), first_leaf)); for leaf in leaves { @@ -479,7 +510,9 @@ impl Tree { } for tree in self.leaves.iter_mut().rev() { - let Some(position) = tree.select_first_match(key) else { continue }; + let Some(position) = tree.select_first_match(key) else { + continue; + }; return Some(position); } diff --git a/src/utils.rs b/src/utils.rs index 99a3a39d..23059915 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,6 @@ use std::borrow::Borrow; use std::io::BufRead; use std::path::Path; -use std::sync::Arc; use anyhow::{Context, Result}; use copypasta::{ClipboardContext, ClipboardProvider}; @@ -9,12 +8,9 @@ use sysinfo::{Disk, DiskExt}; use tuikit::term::Term; use users::{get_current_uid, get_user_by_uid}; -use crate::content_window::RESERVED_ROWS; -use crate::event_dispatch::EventDispatcher; +use crate::content_window::ContentWindow; use crate::fileinfo::human_size; use crate::nvim::nvim; -use crate::status::Status; -use crate::term_manager::{Display, EventReader}; /// Returns a `Display` instance after `tuikit::term::Term` creation. pub fn init_term() -> Result { @@ -59,31 +55,12 @@ pub fn disk_space(disks: &[Disk], path: &Path) -> String { /// Returns `None` if it received `None`. /// It's a poor fix to support OSX where `sysinfo::Disk` doesn't implement `PartialEq`. pub fn opt_mount_point(disk: Option<&Disk>) -> Option<&std::path::Path> { - let Some(disk) = disk else { return None; }; + let Some(disk) = disk else { + return None; + }; Some(disk.mount_point()) } -/// Drops everything holding an `Arc`. -/// If new structs holding `Arc` are introduced -/// (surelly to display something on their own...), we'll have to pass them -/// here and drop them. -/// It's used if the user wants to "cd on quit" which is a nice feature I -/// wanted to implement. -/// Since tuikit term redirects stdout, we have to drop them first. -pub fn drop_everything( - term: Arc, - event_dispatcher: EventDispatcher, - event_reader: EventReader, - status: Status, - display: Display, -) { - std::mem::drop(term); - std::mem::drop(event_dispatcher); - std::mem::drop(event_reader); - std::mem::drop(status); - std::mem::drop(display); -} - /// Print the path on the stdout. pub fn print_on_quit(path_string: &str) { println!("{path_string}") @@ -139,15 +116,19 @@ pub fn extract_lines(content: String) -> Vec { pub fn set_clipboard(content: String) -> Result<()> { log::info!("copied to clipboard: {}", content); - let Ok(mut ctx) = ClipboardContext::new() else { return Ok(()); }; - let Ok(_) = ctx.set_contents(content) else { return Ok(()); }; + let Ok(mut ctx) = ClipboardContext::new() else { + return Ok(()); + }; + let Ok(_) = ctx.set_contents(content) else { + return Ok(()); + }; // For some reason, it's not writen if you don't read it back... let _ = ctx.get_contents(); Ok(()) } -pub fn row_to_index(row: u16) -> usize { - row as usize - RESERVED_ROWS +pub fn row_to_window_index(row: u16) -> usize { + row as usize - ContentWindow::HEADER_ROWS } pub fn string_to_path(path_string: &str) -> Result {