diff --git a/.gitignore b/.gitignore index 222266f..a68a7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ dist/pkgs/arch/*.tar.gz dist/pkgs/ dist/build/macos/openssl/ + +.idea/ +.claude/ + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9ee25f5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,109 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +termscp is a terminal file transfer client with a TUI (Terminal User Interface), supporting SFTP, SCP, FTP/FTPS, Kube, S3, SMB, and WebDAV protocols. It features a dual-pane file explorer (local + remote), bookmarks, system keyring integration, file watching/sync, an embedded terminal, and customizable themes. + +- **Language**: Rust (edition 2024, MSRV 1.89.0) +- **UI Framework**: tuirealm v3 (built on crossterm) +- **File Transfer**: remotefs ecosystem + +## Build & Development Commands + +```bash +# Build +cargo build +cargo build --release +cargo build --no-default-features # minimal build without SMB/keyring + +# Test (CI-equivalent) +cargo test --no-default-features --features github-actions --no-fail-fast + +# Run a single test +cargo test -- --nocapture + +# Run tests for a module +cargo test --lib filetransfer:: +cargo test --lib config::params::tests + +# Lint +cargo clippy -- -Dwarnings + +# Format +cargo fmt --all -- --check # check only +cargo fmt --all # fix +``` + +### System Dependencies (for building) + +- **Linux**: `libdbus-1-dev`, `libsmbclient-dev` +- **macOS**: `pkg-config`, `samba` (brew, with force link) + +## Feature Flags + +- **`smb`** (default): SMB/Samba protocol support +- **`keyring`** (default): System keyring integration for password storage +- **`smb-vendored`**: Vendored SMB library (for static builds) +- **`github-actions`**: CI flag — disables real keyring in tests, uses file-based storage +- **`isolated-tests`**: For parallel test isolation + +## Architecture + +### Application Lifecycle + +``` +main.rs → parse CLI args → ActivityManager::new() → ActivityManager::run() + ↓ + Activity loop (draw → poll → update) + ├── AuthActivity (login/bookmarks) + ├── FileTransferActivity (dual-pane explorer) + └── SetupActivity (configuration) +``` + +`ActivityManager` owns a `Context` that is passed between activities. Each activity takes ownership of the Context on `on_create()` and returns it on `on_destroy()`. + +### Key Modules + +| Module | Path | Purpose | +|--------|------|---------| +| **activity_manager** | `src/activity_manager.rs` | Orchestrates activity lifecycle and transitions | +| **ui/activities** | `src/ui/activities/{auth,filetransfer,setup}/` | Three main screens, each implementing the `Activity` trait | +| **ui/context** | `src/ui/context.rs` | Shared `Context` struct (terminal, config, bookmarks, theme) | +| **filetransfer** | `src/filetransfer/` | Protocol enum, `RemoteFsBuilder`, connection parameters | +| **host** | `src/host/` | `HostBridge` trait — abstracts local (`Localhost`) and remote (`RemoteBridged`) file operations | +| **explorer** | `src/explorer/` | `FileExplorer` — directory navigation, sorting, filtering, transfer queue | +| **system** | `src/system/` | `BookmarksClient`, `ConfigClient`, `ThemeProvider`, `SshKeyStorage`, `KeyStorage` trait | +| **config** | `src/config/` | TOML-based serialization for themes, bookmarks, user params | + +### Core Traits + +- **`Activity`** (`src/ui/activities/mod.rs`): `on_create`, `on_draw`, `will_umount`, `on_destroy` — UI screen lifecycle +- **`HostBridge`** (`src/host/bridge.rs`): Unified file operations interface (connect, list_dir, open_file, mkdir, remove, rename, copy, etc.) +- **`KeyStorage`** (`src/system/keys/mod.rs`): `get_key`/`set_key` — password storage abstraction (keyring or encrypted file fallback) + +### Conditional Compilation + +The `build.rs` defines cfg aliases via `cfg_aliases`: +- `posix`, `macos`, `linux`, `win` — platform shortcuts +- `smb`, `smb_unix`, `smb_windows` — feature + platform combinations + +Platform-specific dependencies: SSH and FTP crates use different TLS backends on Unix vs Windows. SMB support is completely gated behind the `smb` feature flag. + +### File Transfer Protocols + +`FileTransferProtocol` enum maps to protocol-specific parameter types (`ProtocolParams` enum) and `RemoteFsBuilder` constructs the appropriate `RemoteFs` client. Each protocol has its own params struct (e.g., `GenericProtocolParams` for SSH-based, `AwsS3Params`, `KubeProtocolParams`, `SmbParams`, `WebDAVProtocolParams`). + +## Code Conventions + +- **rustfmt**: `group_imports = "StdExternalCrate"`, `imports_granularity = "Module"` +- **Error handling**: Custom error types with `thiserror`, module-level Result aliases (e.g., `HostResult`) +- **Builder pattern**: Used for `RemoteFsBuilder`, `HostBridgeBuilder` +- **Client pattern**: System services wrapped as clients (`BookmarksClient`, `ConfigClient`) +- **Tests**: Unit tests in `#[cfg(test)]` blocks within source files. Tests requiring serial execution use `#[serial]` from `serial_test` +- **Encryption**: Bookmark passwords encrypted with `magic-crypt`; keys stored in system keyring or encrypted file + +## Other conventions + +- Always put plans to `./.claude/plans/` diff --git a/Cargo.lock b/Cargo.lock index e828f5f..e7a5a97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -60,20 +60,19 @@ dependencies = [ [[package]] name = "argh" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240" +checksum = "7f384d96bfd3c0b3c41f24dae69ee9602c091d64fc432225cf5295b5abbe0036" dependencies = [ "argh_derive", "argh_shared", - "rust-fuzzy-search", ] [[package]] name = "argh_derive" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803" +checksum = "938e5f66269c1f168035e29ed3fb437b084e476465e9314a0328f4005d7be599" dependencies = [ "argh_shared", "proc-macro2", @@ -83,9 +82,9 @@ dependencies = [ [[package]] name = "argh_shared" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6" +checksum = "5127f8a5bc1cfb0faf1f6248491452b8a5b6901068d8da2d47cbb285986ae683" dependencies = [ "serde", ] @@ -110,9 +109,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.12" +version = "1.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" dependencies = [ "aws-credential-types", "aws-runtime", @@ -152,9 +151,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" +checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" dependencies = [ "aws-lc-sys", "zeroize", @@ -162,9 +161,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.35.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -174,9 +173,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.5.17" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -190,7 +189,9 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "http-body 0.4.6", + "http-body 1.0.1", "percent-encoding", "pin-project-lite", "tracing", @@ -199,9 +200,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.118.0" +version = "1.122.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e6b7079f85d9ea9a70643c9f89f50db70f5ada868fa9cfe08c1ffdf51abc13" +checksum = "94c2ca0cba97e8e279eb6c0b2d0aa10db5959000e602ab2b7c02de6b85d4c19b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -211,6 +212,7 @@ dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -222,8 +224,8 @@ dependencies = [ "hmac", "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", - "lru", + "http-body 1.0.1", + "lru 0.16.3", "percent-encoding", "regex-lite", "sha2", @@ -233,15 +235,16 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.91.0" +version = "1.93.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee6402a36f27b52fe67661c6732d684b2635152b676aa2babbfb5204f99115d" +checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -249,21 +252,23 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-ssooidc" -version = "1.93.0" +version = "1.95.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45a7f750bbd170ee3677671ad782d90b894548f4e4ae168302c57ec9de5cb3e" +checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", @@ -271,21 +276,23 @@ dependencies = [ "bytes", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sdk-sts" -version = "1.95.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55542378e419558e6b1f398ca70adb0b2088077e79ad9f14eb09441f2f7b2164" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" dependencies = [ "aws-credential-types", "aws-runtime", "aws-smithy-async", "aws-smithy-http", "aws-smithy-json", + "aws-smithy-observability", "aws-smithy-query", "aws-smithy-runtime", "aws-smithy-runtime-api", @@ -294,15 +301,16 @@ dependencies = [ "aws-types", "fastrand", "http 0.2.12", + "http 1.4.0", "regex-lite", "tracing", ] [[package]] name = "aws-sigv4" -version = "1.3.7" +version = "1.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -328,9 +336,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.7" +version = "1.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" dependencies = [ "futures-util", "pin-project-lite", @@ -339,17 +347,18 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.63.12" +version = "0.64.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" +checksum = "ddcf418858f9f3edd228acb8759d77394fed7531cce78d02bdda499025368439" dependencies = [ "aws-smithy-http", "aws-smithy-types", "bytes", "crc-fast", "hex", - "http 0.2.12", - "http-body 0.4.6", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", "md-5", "pin-project-lite", "sha1", @@ -359,9 +368,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.14" +version = "0.60.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650" +checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" dependencies = [ "aws-smithy-types", "bytes", @@ -370,9 +379,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.62.6" +version = "0.63.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -381,9 +390,9 @@ dependencies = [ "bytes-utils", "futures-core", "futures-util", - "http 0.2.12", "http 1.4.0", - "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", "percent-encoding", "pin-project-lite", "pin-utils", @@ -392,15 +401,15 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" +checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.27", - "h2 0.4.12", + "h2 0.4.13", "http 0.2.12", "http 1.4.0", "http-body 0.4.6", @@ -411,38 +420,38 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.35", - "rustls-native-certs 0.8.2", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.2", + "tower 0.5.3", "tracing", ] [[package]] name = "aws-smithy-json" -version = "0.61.9" +version = "0.62.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.1.5" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.9" +version = "0.60.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" dependencies = [ "aws-smithy-types", "urlencoding", @@ -450,9 +459,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.9.6" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fda37911905ea4d3141a01364bc5509a0f32ae3f3b22d6e330c0abfb62d247" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -466,6 +475,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "http-body 1.0.1", + "http-body-util", "pin-project-lite", "pin-utils", "tokio", @@ -474,9 +484,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.9.3" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -491,9 +501,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.3.5" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" dependencies = [ "base64-simd", "bytes", @@ -568,9 +578,9 @@ dependencies = [ [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -580,9 +590,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "block-buffer" @@ -613,9 +623,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -625,9 +635,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "bytes-utils" @@ -665,25 +675,26 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.9" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" dependencies = [ "serde", + "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.19.2" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -712,9 +723,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.50" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -735,10 +746,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "chrono" -version = "0.4.42" +name = "chacha20" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -854,10 +876,19 @@ dependencies = [ ] [[package]] -name = "crc" -version = "3.4.0" +name = "cpufeatures" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" dependencies = [ "crc-catalog", ] @@ -879,15 +910,14 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc-fast" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ "crc", "digest", - "rand 0.9.2", - "regex", "rustversion", + "spin", ] [[package]] @@ -930,7 +960,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "mio", "parking_lot", @@ -946,13 +976,13 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "derive_more", "document-features", "mio", "parking_lot", - "rustix 1.1.2", + "rustix 1.1.4", "signal-hook", "signal-hook-mio", "winapi", @@ -1006,7 +1036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1032,8 +1062,18 @@ version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] @@ -1050,22 +1090,46 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ - "darling_core", + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", "quote", "syn", ] [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "dbus" @@ -1117,9 +1181,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -1150,7 +1214,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ - "darling", + "darling 0.20.11", "proc-macro2", "quote", "syn", @@ -1168,18 +1232,18 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", @@ -1237,11 +1301,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -1405,27 +1469,26 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1443,6 +1506,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1484,13 +1553,12 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1499,9 +1567,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1509,15 +1577,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1526,9 +1594,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1545,9 +1613,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1556,21 +1624,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -1580,7 +1648,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1596,14 +1663,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -1622,16 +1689,30 @@ dependencies = [ ] [[package]] -name = "git2" -version = "0.20.3" +name = "getrandom" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ - "bitflags 2.10.0", + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "git2" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" +dependencies = [ + "bitflags 2.11.0", "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -1674,9 +1755,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -1699,7 +1780,7 @@ checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", - "foldhash", + "foldhash 0.1.5", ] [[package]] @@ -1707,6 +1788,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "headers" @@ -1874,7 +1960,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "httparse", @@ -1931,8 +2017,8 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "log", - "rustls 0.23.35", - "rustls-native-certs 0.8.2", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -1968,14 +2054,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", @@ -1984,7 +2069,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2 0.6.2", "tokio", "tower-service", "tracing", @@ -1992,9 +2077,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -2095,6 +2180,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2124,12 +2215,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -2160,7 +2253,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -2186,11 +2279,11 @@ dependencies = [ [[package]] name = "instability" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6778b0196eefee7df739db78758e5cf9b37412268bfa5650bfeed028aed20d9c" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", @@ -2205,9 +2298,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -2243,9 +2336,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jobserver" @@ -2259,9 +2352,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" dependencies = [ "once_cell", "wasm-bindgen", @@ -2306,7 +2399,7 @@ dependencies = [ "log", "openssl", "security-framework 2.11.1", - "security-framework 3.5.1", + "security-framework 3.7.0", "windows-sys 0.60.2", "zeroize", ] @@ -2367,7 +2460,7 @@ dependencies = [ "kube-core", "pem", "rand 0.8.5", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pemfile 2.2.0", "secrecy", "serde", @@ -2399,9 +2492,9 @@ dependencies = [ [[package]] name = "lazy-regex" -version = "3.4.2" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191898e17ddee19e60bccb3945aa02339e81edd4a8c50e21fd4d48cdecda7b29" +checksum = "6bae91019476d3ec7147de9aa291cadb6d870abf2f3015d2da73a90325ac1496" dependencies = [ "lazy-regex-proc_macros", "once_cell", @@ -2410,9 +2503,9 @@ dependencies = [ [[package]] name = "lazy-regex-proc_macros" -version = "3.4.2" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c35dc8b0da83d1a9507e12122c80dea71a9c7c613014347392483a83ea593e04" +checksum = "4de9c1e1439d8b7b3061b2d209809f447ca33241733d9a3c01eabf2dc8d94358" dependencies = [ "proc-macro2", "quote", @@ -2427,10 +2520,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.178" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libdbus-sys" @@ -2458,13 +2557,13 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall 0.6.0", + "redox_syscall 0.7.3", ] [[package]] @@ -2508,9 +2607,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "4735e9cbde5aac84a5ce588f6b23a90b9b0b528f6c5a8db8a4aff300463a0839" dependencies = [ "cc", "libc", @@ -2526,9 +2625,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2566,6 +2665,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -2612,9 +2720,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "mime" @@ -2640,23 +2748,23 @@ checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] @@ -2673,7 +2781,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -2687,9 +2795,9 @@ dependencies = [ [[package]] name = "notify-rust" -version = "4.11.7" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" dependencies = [ "dbus", "futures-lite", @@ -2700,15 +2808,18 @@ dependencies = [ [[package]] name = "notify-types" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] [[package]] name = "ntapi" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" dependencies = [ "winapi", ] @@ -2736,9 +2847,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2775,9 +2886,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2788,7 +2899,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -2805,13 +2916,32 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2835,7 +2965,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -2862,10 +2992,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "openssl-src" -version = "300.5.4+3.5.4" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.5.5+3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" dependencies = [ "cc", ] @@ -2973,7 +3109,7 @@ dependencies = [ "libc", "log", "pavao-sys", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3017,9 +3153,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", @@ -3027,9 +3163,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" dependencies = [ "pest", "pest_generator", @@ -3037,9 +3173,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" dependencies = [ "pest", "pest_meta", @@ -3050,9 +3186,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", "sha2", @@ -3060,18 +3196,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", @@ -3080,9 +3216,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3118,9 +3254,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -3157,10 +3293,20 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.103" +name = "prettyplease" +version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3195,9 +3341,9 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.35", - "socket2 0.6.1", - "thiserror 2.0.17", + "rustls 0.23.37", + "socket2 0.6.2", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3215,10 +3361,10 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3233,16 +3379,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2 0.6.2", "tracing", "windows-sys 0.60.2", ] [[package]] name = "quote" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -3271,7 +3417,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.0", ] [[package]] @@ -3291,7 +3448,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3300,32 +3457,38 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + [[package]] name = "ratatui" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cassowary", "compact_str", "crossterm 0.28.1", "indoc", "instability", "itertools", - "lru", + "lru 0.12.5", "paste", "strum", "unicode-segmentation", @@ -3359,16 +3522,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] name = "redox_syscall" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3377,16 +3540,16 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3396,9 +3559,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3407,15 +3570,15 @@ dependencies = [ [[package]] name = "regex-lite" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "remotefs" @@ -3440,15 +3603,15 @@ dependencies = [ "log", "path-slash", "remotefs", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] [[package]] name = "remotefs-ftp" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c84ed1367170d6f589f09998b648e7d9bc09d93091130293be8ff09b7a8329" +checksum = "107c234184fd822d1270c482eb8a42354887479cb9fe7834c29655f31e80a4bb" dependencies = [ "log", "path-slash", @@ -3493,9 +3656,9 @@ dependencies = [ [[package]] name = "remotefs-ssh" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca8b65fbd60801ac03973a41196b030fb04d1ce1af33d3c58c5bad94d46eb27" +checksum = "83368dcb24c53a5a44ceba93960a60213f557dcef1a58a81ec6851eeab3ec764" dependencies = [ "chrono", "lazy-regex", @@ -3570,16 +3733,16 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.26" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", "futures-core", "futures-util", - "h2 0.4.12", + "h2 0.4.13", "http 1.4.0", "http-body 1.0.1", "http-body-util", @@ -3591,7 +3754,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.35", + "rustls 0.23.37", "rustls-pki-types", "serde", "serde_json", @@ -3599,7 +3762,7 @@ dependencies = [ "sync_wrapper 1.0.2", "tokio", "tokio-rustls 0.26.4", - "tower 0.5.2", + "tower 0.5.3", "tower-http 0.6.8", "tower-service", "url", @@ -3628,7 +3791,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3655,12 +3818,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rust-fuzzy-search" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3682,7 +3839,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3691,14 +3848,14 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", - "linux-raw-sys 0.11.0", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] @@ -3716,16 +3873,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.8", + "rustls-webpki 0.103.9", "subtle", "zeroize", ] @@ -3736,7 +3893,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe", + "openssl-probe 0.1.6", "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", @@ -3745,14 +3902,14 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework 3.7.0", ] [[package]] @@ -3775,9 +3932,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -3795,9 +3952,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -3822,9 +3979,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3905,7 +4062,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3914,11 +4071,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3927,9 +4084,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3959,7 +4116,7 @@ dependencies = [ "log", "quick-xml 0.37.5", "regex", - "reqwest 0.12.26", + "reqwest 0.12.28", "self-replace", "semver", "serde_json", @@ -4022,15 +4179,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -4069,11 +4226,12 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" dependencies = [ - "futures", + "futures-executor", + "futures-util", "log", "once_cell", "parking_lot", @@ -4083,9 +4241,9 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" dependencies = [ "proc-macro2", "quote", @@ -4099,7 +4257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -4110,15 +4268,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] [[package]] name = "shellexpand" -version = "3.1.1" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" +checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8" dependencies = [ "dirs", ] @@ -4152,10 +4310,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4198,9 +4357,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -4226,14 +4385,20 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.6.0" @@ -4260,7 +4425,7 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libssh2-sys", "parking_lot", @@ -4268,17 +4433,17 @@ dependencies = [ [[package]] name = "ssh2-config" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc7aae258493fa8ea06796b133b9076f3002c46cd1a4085ddd6df7236fee7034" +checksum = "52b8139953690127e92a058701bc65c0ea292c08084c6136fa052021373d739c" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "dirs", "git2", "glob", "log", - "thiserror 2.0.17", + "thiserror 2.0.18", "wildmatch", ] @@ -4330,23 +4495,23 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "suppaftp" -version = "7.0.7" +version = "8.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8928c89e226be233f0eb1594e9bd023f72a948dc06581c0d908387f57de1de" +checksum = "7d3da253d7e9993de86df41eb89e8cb1b6f567abe215798645651fca4148d0aa" dependencies = [ "chrono", "futures-lite", "lazy-regex", "log", "native-tls", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4381,15 +4546,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.34.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", - "windows 0.57.0", + "objc2-io-kit", + "windows", ] [[package]] @@ -4431,21 +4597,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.17", - "windows 0.61.3", + "thiserror 2.0.18", + "windows", "windows-version", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.1", "once_cell", - "rustix 1.1.2", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -4463,7 +4629,7 @@ name = "termscp" version = "0.19.1" dependencies = [ "argh", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytesize", "cfg_aliases", "chrono", @@ -4482,7 +4648,7 @@ dependencies = [ "nucleo", "open", "pretty_assertions", - "rand 0.9.2", + "rand 0.10.0", "regex", "remotefs", "remotefs-aws-s3", @@ -4499,7 +4665,7 @@ dependencies = [ "simplelog", "ssh2-config", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "toml", "tui-realm-stdlib", @@ -4535,11 +4701,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4555,9 +4721,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -4575,9 +4741,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4585,22 +4751,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4633,16 +4799,16 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.1", + "socket2 0.6.2", "tokio-macros", "windows-sys 0.61.2", ] @@ -4684,7 +4850,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.35", + "rustls 0.23.37", "tokio", ] @@ -4702,9 +4868,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4715,9 +4881,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "1.0.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" dependencies = [ "indexmap", "serde_core", @@ -4730,18 +4896,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -4771,9 +4937,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4791,7 +4957,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ "base64 0.21.7", - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "http 1.4.0", "http-body 1.0.1", @@ -4809,14 +4975,14 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.4.0", "http-body 1.0.1", "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower 0.5.3", "tower-layer", "tower-service", ] @@ -4898,12 +5064,12 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3a96abaf33552e9e2487bf56f537e49fad1d1fbc55a3fd542449d4e30e7c8f3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm 0.29.0", "dyn-clone", "lazy-regex", "ratatui", - "thiserror 2.0.17", + "thiserror 2.0.18", "tuirealm_derive", ] @@ -4950,9 +5116,9 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-linebreak" @@ -4989,6 +5155,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -5003,9 +5175,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", @@ -5039,9 +5211,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "js-sys", "wasm-bindgen", @@ -5065,9 +5237,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" dependencies = [ "anyhow", "cargo_metadata", @@ -5082,9 +5254,9 @@ dependencies = [ [[package]] name = "vergen-git2" -version = "1.0.7" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f6ee511ec45098eabade8a0750e76eec671e7fb2d9360c563911336bea9cac1" +checksum = "d51ab55ddf1188c8d679f349775362b0fa9e90bd7a4ac69838b2a087623f0d57" dependencies = [ "anyhow", "derive_builder", @@ -5097,9 +5269,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" dependencies = [ "anyhow", "derive_builder", @@ -5183,25 +5355,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" +name = "wasi" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasite" -version = "0.1.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.14.7+wasi-0.2.4", +] [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" dependencies = [ "cfg-if", "once_cell", @@ -5212,11 +5405,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "8a89f4650b770e4521aa6573724e2aed4704372151bd0de9d16a3bbabb87441a" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5225,9 +5419,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5235,9 +5429,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" dependencies = [ "bumpalo", "proc-macro2", @@ -5248,18 +5442,52 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" dependencies = [ "unicode-ident", ] [[package]] -name = "web-sys" -version = "0.3.83" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "705eceb4ce901230f8625bd1d665128056ccbe4b7408faa625eec1ba80f59a97" dependencies = [ "js-sys", "wasm-bindgen", @@ -5277,9 +5505,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] @@ -5298,11 +5526,13 @@ dependencies = [ [[package]] name = "whoami" -version = "1.6.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" dependencies = [ + "libc", "libredox", + "objc2-system-configuration", "wasite", "web-sys", ] @@ -5344,16 +5574,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -5376,26 +5596,14 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -5407,8 +5615,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -5425,17 +5633,6 @@ dependencies = [ "windows-threading", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -5447,17 +5644,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -5491,15 +5677,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -5803,9 +5980,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -5820,7 +6079,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix 1.1.2", + "rustix 1.1.4", ] [[package]] @@ -5860,18 +6119,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", @@ -5910,9 +6169,9 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", @@ -5965,7 +6224,7 @@ dependencies = [ "flate2", "indexmap", "memchr", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "zopfli", ] @@ -5978,9 +6237,15 @@ checksum = "dba6063ff82cdbd9a765add16d369abe81e520f836054e997c2db217ceca40c0" dependencies = [ "base64 0.22.1", "ed25519-dalek", - "thiserror 2.0.17", + "thiserror 2.0.18", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index dae7913..65cf2d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ notify = "8" notify-rust = { version = "^4", default-features = false, features = ["d"] } nucleo = "0.5" open = "5" -rand = "^0.9" +rand = "0.10" regex = "^1" remotefs = "^0.3" remotefs-aws-s3 = "0.4" @@ -73,21 +73,21 @@ self_update = { version = "^0.42", default-features = false, features = [ serde = { version = "^1", features = ["derive"] } shellexpand = "3" simplelog = "^0.12" -ssh2-config = "^0.6" +ssh2-config = "^0.7" tempfile = "3" thiserror = "2" tokio = { version = "1", features = ["rt"] } -toml = "^0.9" +toml = "1" tui-realm-stdlib = "3" tuirealm = "3" tui-term = "0.2" unicode-width = "0.2" version-compare = "^0.2" -whoami = "^1.6" +whoami = "2" wildmatch = "2" [target."cfg(target_family = \"unix\")".dependencies] -remotefs-ftp = { version = "^0.3", features = [ +remotefs-ftp = { version = "^0.4", features = [ "native-tls-vendored", "native-tls", ] } @@ -98,7 +98,7 @@ remotefs-ssh = { version = "^0.7", default-features = false, features = [ uzers = "0.12" [target."cfg(target_family = \"windows\")".dependencies] -remotefs-ftp = { version = "^0.3", features = ["native-tls"] } +remotefs-ftp = { version = "^0.4", features = ["native-tls"] } remotefs-ssh = { version = "^0.7" } [dev-dependencies] @@ -107,7 +107,7 @@ serial_test = "^3" [build-dependencies] cfg_aliases = "0.2" -vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] } +vergen-git2 = { version = "9", features = ["build", "cargo", "rustc", "si"] } [features] default = ["smb", "keyring"] diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..50082c8 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,48 @@ +[changelog] +body = """ +## {{ version | trim_start_matches(pat="v") }} + +Released on {{ timestamp | date(format="%Y-%m-%d") }} +{{ "" }} +{%- if commits | filter(attribute="breaking", value=true) | length > 0 %} + +### ⚠ Breaking Changes + +{%- for commit in commits | filter(attribute="breaking", value=true) %} +- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }} + {%- if commit.breaking_description %} + > {{ commit.breaking_description }} + {%- endif %} +{%- endfor %} +{%- endif %} +{%- for group, commits in commits | group_by(attribute="group") %} + +### {{ group | upper_first }} + +{%- for commit in commits %} +- {% if commit.breaking %}💥 {% endif %}{% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | split(pat="\n") | first | trim }} + {%- if commit.body %} + > {{ commit.body | split(pat="\n") | join(sep="\n > ") }} + {%- endif %} +{%- endfor %} +{%- endfor %} +""" +trim = false + +[git] +conventional_commits = true +filter_unconventional = true +split_commits = false +commit_parsers = [ + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^refactor", group = "Changed" }, + { message = "^perf", group = "Performance" }, + { message = "^doc", group = "Documentation" }, + { message = "^test", group = "Testing" }, + { message = "^ci", group = "CI" }, + { message = "^chore", group = "Miscellaneous" }, +] +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "oldest" diff --git a/src/activity_manager.rs b/src/activity_manager.rs index 38ed111..5083a13 100644 --- a/src/activity_manager.rs +++ b/src/activity_manager.rs @@ -187,14 +187,15 @@ impl ActivityManager { // * if protocol is SCP or SFTP check whether a SSH key is registered for this remote, in case not ask password let storage = SshKeyStorage::from(self.context.as_ref().unwrap().config()); let generic_params = params.generic_params().unwrap(); + let username = generic_params + .username + .clone() + .map(Ok) + .unwrap_or_else(whoami::username) + .map_err(|err| format!("Could not get current username: {err}"))?; + if storage - .resolve( - &generic_params.address, - &generic_params - .username - .clone() - .unwrap_or(whoami::username()), - ) + .resolve(&generic_params.address, &username) .is_none() { debug!( diff --git a/src/filetransfer/params.rs b/src/filetransfer/params.rs index 72699a1..7f96018 100644 --- a/src/filetransfer/params.rs +++ b/src/filetransfer/params.rs @@ -35,7 +35,7 @@ impl HostBridgeParams { /// Returns the host name for the bridge params pub fn username(&self) -> Option { match self { - HostBridgeParams::Localhost(_) => Some(whoami::username()), + HostBridgeParams::Localhost(_) => whoami::username().ok(), HostBridgeParams::Remote(_, params) => { params.generic_params().and_then(|p| p.username.clone()) } diff --git a/src/filetransfer/remotefs_builder.rs b/src/filetransfer/remotefs_builder.rs index 5c58fbc..d5ff51a 100644 --- a/src/filetransfer/remotefs_builder.rs +++ b/src/filetransfer/remotefs_builder.rs @@ -173,18 +173,16 @@ impl RemoteFsBuilder { credentials = credentials.workgroup(workgroup); } - match SmbFs::try_new( + SmbFs::try_new( credentials, SmbOptions::default() .one_share_per_server(true) .case_sensitive(false), - ) { - Ok(fs) => fs, - Err(e) => { - error!("Invalid params for protocol SMB: {e}"); - panic!("Invalid params for protocol SMB: {e}") - } - } + ) + .unwrap_or_else(|e| { + error!("Invalid params for protocol SMB: {e}"); + panic!("Invalid params for protocol SMB: {e}") + }) } #[cfg(smb_windows)] @@ -236,12 +234,13 @@ impl RemoteFsBuilder { } else { //* case 3: use system username; can't be None debug!("no username was provided, using current username"); - opts = opts.username(whoami::username()); + if let Ok(username) = whoami::username() { + opts = opts.username(username); + } } - } else { - //* case 3: use system username; can't be None + } else if let Ok(username) = whoami::username() { debug!("no username was provided, using current username"); - opts = opts.username(whoami::username()); + opts = opts.username(username); } // For SSH protocols, only set password if explicitly provided and non-empty. // This allows the SSH library to prioritize key-based and agent authentication. diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index cdd2eea..9fb290f 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -2,8 +2,6 @@ //! //! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system -// Crate -// Ext use std::fs::OpenOptions; use std::path::{Path, PathBuf}; use std::string::ToString; @@ -12,11 +10,8 @@ use std::time::SystemTime; use super::keys::filestorage::FileStorage; use super::keys::keyringstorage::KeyringStorage; use super::keys::{KeyStorage, KeyStorageError}; -// Local -use crate::config::{ - bookmarks::{Bookmark, UserHosts}, - serialization::{SerializerError, SerializerErrorKind, deserialize, serialize}, -}; +use crate::config::bookmarks::{Bookmark, UserHosts}; +use crate::config::serialization::{SerializerError, SerializerErrorKind, deserialize, serialize}; use crate::filetransfer::FileTransferParams; use crate::utils::crypto; use crate::utils::fmt::fmt_time; @@ -104,7 +99,7 @@ impl BookmarksClient { fn keyring(storage_path: &Path, keyring: bool) -> (Box, &'static str) { if keyring && cfg!(feature = "keyring") { debug!("Setting up KeyStorage"); - let username: String = whoami::username(); + let username = whoami::username().unwrap_or_default(); let storage: KeyringStorage = KeyringStorage::new(username.as_str()); // Check if keyring storage is supported #[cfg(not(test))] diff --git a/src/system/keys/keyringstorage.rs b/src/system/keys/keyringstorage.rs index f79bb54..9f980ac 100644 --- a/src/system/keys/keyringstorage.rs +++ b/src/system/keys/keyringstorage.rs @@ -78,14 +78,13 @@ impl KeyStorage for KeyringStorage { mod tests { #[test] #[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))] - fn test_system_keys_keyringstorage() { + fn test_system_keys_keyring_storage() { use pretty_assertions::assert_eq; - use whoami::username; use super::*; - let username: String = username(); - let storage: KeyringStorage = KeyringStorage::new(username.as_str()); + let username = whoami::username().expect("no username"); + let storage = KeyringStorage::new(username.as_str()); assert!(storage.is_supported()); let app_name: &str = "termscp-test2"; let secret: &str = "Th15-15/My-Супер-Секрет"; diff --git a/src/ui/activities/filetransfer/actions/change_dir.rs b/src/ui/activities/filetransfer/actions/change_dir.rs index 3cd091b..c1e1cc9 100644 --- a/src/ui/activities/filetransfer/actions/change_dir.rs +++ b/src/ui/activities/filetransfer/actions/change_dir.rs @@ -1,13 +1,13 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::PathBuf; use remotefs::File; -use super::{FileExplorerTab, FileTransferActivity, LogLevel, Msg, PendingActionMsg}; +use super::{FileTransferActivity, LogLevel, Msg, PendingActionMsg}; /// Describes destination for sync browsing enum SyncBrowsingDestination { @@ -17,47 +17,29 @@ enum SyncBrowsingDestination { } impl FileTransferActivity { - /// Enter a directory on local host from entry - pub(crate) fn action_enter_local_dir(&mut self, dir: File) { - self.host_bridge_changedir(dir.path(), true); + /// Enter a directory from entry, dispatching via the active tab's pane. + pub(crate) fn action_enter_dir(&mut self, dir: File) { + self.pane_changedir(dir.path(), true); if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); } } - /// Enter a directory on local host from entry - pub(crate) fn action_enter_remote_dir(&mut self, dir: File) { - self.remote_changedir(dir.path(), true); - if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::Path(dir.name())); - } - } - - /// Change local directory reading value from input - pub(crate) fn action_change_local_dir(&mut self, input: String) { - let dir_path: PathBuf = - self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path()); - self.host_bridge_changedir(dir_path.as_path(), true); + /// Change directory reading value from input, dispatching via the active tab's pane. + pub(crate) fn action_change_dir(&mut self, input: String) { + let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path()); + self.pane_changedir(dir_path.as_path(), true); // Check whether to sync if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::Path(input)); } } - /// Change remote directory reading value from input - pub(crate) fn action_change_remote_dir(&mut self, input: String) { - let dir_path: PathBuf = self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path()); - self.remote_changedir(dir_path.as_path(), true); - // Check whether to sync - if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::Path(input)); - } - } - - /// Go to previous directory from localhost - pub(crate) fn action_go_to_previous_local_dir(&mut self) { - if let Some(d) = self.host_bridge_mut().popd() { - self.host_bridge_changedir(d.as_path(), false); + /// Go to previous directory, dispatching via the active tab's pane. + pub(crate) fn action_go_to_previous_dir(&mut self) { + let prev = self.browser.fs_pane_mut().explorer.popd(); + if let Some(d) = prev { + self.pane_changedir(d.as_path(), false); // Check whether to sync if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); @@ -65,41 +47,12 @@ impl FileTransferActivity { } } - /// Go to previous directory from remote host - pub(crate) fn action_go_to_previous_remote_dir(&mut self) { - if let Some(d) = self.remote_mut().popd() { - self.remote_changedir(d.as_path(), false); - // Check whether to sync - if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::PreviousDir); - } - } - } - - /// Go to upper directory on local host - pub(crate) fn action_go_to_local_upper_dir(&mut self) { - // Get pwd - let path: PathBuf = self.host_bridge().wrkdir.clone(); - // Go to parent directory + /// Go to upper directory, dispatching via the active tab's pane. + pub(crate) fn action_go_to_upper_dir(&mut self) { + let path = self.browser.fs_pane().explorer.wrkdir.clone(); if let Some(parent) = path.as_path().parent() { - self.host_bridge_changedir(parent, true); - // If sync is enabled update remote too - if self.browser.sync_browsing && self.browser.found().is_none() { - self.synchronize_browsing(SyncBrowsingDestination::ParentDir); - } - } - } - - /// #### action_go_to_remote_upper_dir - /// - /// Go to upper directory on remote host - pub(crate) fn action_go_to_remote_upper_dir(&mut self) { - // Get pwd - let path: PathBuf = self.remote().wrkdir.clone(); - // Go to parent directory - if let Some(parent) = path.as_path().parent() { - self.remote_changedir(parent, true); - // If sync is enabled update local too + self.pane_changedir(parent, true); + // If sync is enabled update the other side too if self.browser.sync_browsing && self.browser.found().is_none() { self.synchronize_browsing(SyncBrowsingDestination::ParentDir); } @@ -117,9 +70,11 @@ impl FileTransferActivity { None => return, }; trace!("Synchronizing browsing to path {}", path.display()); - // Check whether destination exists on host - let exists = match self.browser.tab() { - FileExplorerTab::HostBridge => match self.client.exists(path.as_path()) { + // Check whether destination exists on the opposite side + let is_local = self.is_local_tab(); + let exists = if is_local { + // Current tab is local, so the opposite is remote + match self.browser.remote_pane_mut().fs.exists(path.as_path()) { Ok(e) => e, Err(err) => { error!( @@ -129,8 +84,10 @@ impl FileTransferActivity { ); return; } - }, - FileExplorerTab::Remote => match self.host_bridge.exists(path.as_path()) { + } + } else { + // Current tab is remote, so the opposite is local + match self.browser.local_pane_mut().fs.exists(path.as_path()) { Ok(e) => e, Err(err) => { error!( @@ -140,8 +97,7 @@ impl FileTransferActivity { ); return; } - }, - _ => return, + } }; let name = path .file_name() @@ -159,12 +115,8 @@ impl FileTransferActivity { ]) == Msg::PendingAction(PendingActionMsg::MakePendingDirectory) { trace!("User wants to create the unexisting directory"); - // Make directory - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_remote_mkdir(name.clone()), - FileExplorerTab::Remote => self.action_local_mkdir(name.clone()), - _ => {} - } + // Make directory on the opposite side + self.sync_mkdir_on_opposite(name.clone()); } else { // Do not synchronize, disable sync browsing and return trace!( @@ -183,23 +135,21 @@ impl FileTransferActivity { self.umount_sync_browsing_mkdir_popup(); } trace!("Entering on the other explorer directory {}", name); - // Enter directory - match destination { - SyncBrowsingDestination::ParentDir => match self.browser.tab() { - FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true), - FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true), - _ => {} - }, - SyncBrowsingDestination::Path(_) => match self.browser.tab() { - FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), true), - FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), true), - _ => {} - }, - SyncBrowsingDestination::PreviousDir => match self.browser.tab() { - FileExplorerTab::HostBridge => self.remote_changedir(path.as_path(), false), - FileExplorerTab::Remote => self.host_bridge_changedir(path.as_path(), false), - _ => {} - }, + // Enter directory on the opposite side + let push = !matches!(destination, SyncBrowsingDestination::PreviousDir); + if is_local { + self.remote_changedir(path.as_path(), push); + } else { + self.local_changedir(path.as_path(), push); + } + } + + /// Create a directory on the opposite side for sync browsing. + fn sync_mkdir_on_opposite(&mut self, name: String) { + if self.is_local_tab() { + self.action_remote_mkdir(name); + } else { + self.action_local_mkdir(name); } } @@ -208,35 +158,34 @@ impl FileTransferActivity { &mut self, destination: &SyncBrowsingDestination, ) -> Option { - match (destination, self.browser.tab()) { - // NOTE: tab and methods are switched on purpose - (SyncBrowsingDestination::ParentDir, FileExplorerTab::HostBridge) => { - self.remote().wrkdir.parent().map(|x| x.to_path_buf()) - } - (SyncBrowsingDestination::ParentDir, FileExplorerTab::Remote) => { - self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf()) - } - (SyncBrowsingDestination::PreviousDir, FileExplorerTab::HostBridge) => { - if let Some(p) = self.remote_mut().popd() { - Some(p) + let is_local = self.is_local_tab(); + match destination { + // NOTE: tab and methods are switched on purpose (we resolve from the opposite side) + SyncBrowsingDestination::ParentDir => { + if is_local { + self.remote().wrkdir.parent().map(|x| x.to_path_buf()) } else { - warn!("Cannot synchronize browsing: remote has no previous directory in stack"); - None + self.host_bridge().wrkdir.parent().map(|x| x.to_path_buf()) } } - (SyncBrowsingDestination::PreviousDir, FileExplorerTab::Remote) => { - if let Some(p) = self.host_bridge_mut().popd() { + SyncBrowsingDestination::PreviousDir => { + if is_local { + if let Some(p) = self.remote_mut().popd() { + Some(p) + } else { + warn!( + "Cannot synchronize browsing: remote has no previous directory in stack" + ); + None + } + } else if let Some(p) = self.host_bridge_mut().popd() { Some(p) } else { warn!("Cannot synchronize browsing: local has no previous directory in stack"); None } } - (SyncBrowsingDestination::Path(p), _) => Some(PathBuf::from(p.as_str())), - _ => { - warn!("Cannot synchronize browsing for current explorer"); - None - } + SyncBrowsingDestination::Path(p) => Some(PathBuf::from(p.as_str())), } } } diff --git a/src/ui/activities/filetransfer/actions/chmod.rs b/src/ui/activities/filetransfer/actions/chmod.rs index 61de0a7..e4dc36d 100644 --- a/src/ui/activities/filetransfer/actions/chmod.rs +++ b/src/ui/activities/filetransfer/actions/chmod.rs @@ -3,83 +3,12 @@ use remotefs::fs::UnixPex; use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - pub fn action_local_chmod(&mut self, mode: UnixPex) { - let files = self.get_local_selected_entries().get_files(); + /// Change file mode for the currently selected entries via the active tab's pane. + pub(crate) fn action_chmod(&mut self, mode: UnixPex) { + let files = self.get_selected_entries().get_files(); for file in files { - if let Err(err) = self.host_bridge.chmod(file.path(), mode) { - self.log_and_alert( - LogLevel::Error, - format!( - "could not change mode for {}: {}", - file.path().display(), - err - ), - ); - return; - } - self.log( - LogLevel::Info, - format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), - ); - } - } - - pub fn action_remote_chmod(&mut self, mode: UnixPex) { - let files = self.get_remote_selected_entries().get_files(); - - for file in files { - let mut metadata = file.metadata.clone(); - metadata.mode = Some(mode); - - if let Err(err) = self.client.setstat(file.path(), metadata) { - self.log_and_alert( - LogLevel::Error, - format!( - "could not change mode for {}: {}", - file.path().display(), - err - ), - ); - return; - } - self.log( - LogLevel::Info, - format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), - ); - } - } - - pub fn action_find_local_chmod(&mut self, mode: UnixPex) { - let files = self.get_found_selected_entries().get_files(); - - for file in files { - if let Err(err) = self.host_bridge.chmod(file.path(), mode) { - self.log_and_alert( - LogLevel::Error, - format!( - "could not change mode for {}: {}", - file.path().display(), - err - ), - ); - return; - } - self.log( - LogLevel::Info, - format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), - ); - } - } - - pub fn action_find_remote_chmod(&mut self, mode: UnixPex) { - let files = self.get_found_selected_entries().get_files(); - - for file in files { - let mut metadata = file.metadata.clone(); - metadata.mode = Some(mode); - - if let Err(err) = self.client.setstat(file.path(), metadata) { + if let Err(err) = self.browser.fs_pane_mut().fs.chmod(file.path(), mode) { self.log_and_alert( LogLevel::Error, format!( diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 01f1511..60a5a34 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -1,61 +1,38 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::{Path, PathBuf}; -use remotefs::{File, RemoteErrorType}; +use remotefs::File; use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; impl FileTransferActivity { - /// Copy file on local - pub(crate) fn action_local_copy(&mut self, input: String) { - match self.get_local_selected_entries() { + /// Copy the currently selected file(s) via the active tab's pane. + pub(crate) fn action_copy(&mut self, input: String) { + match self.get_selected_entries() { SelectedFile::One(entry) => { - let dest_path: PathBuf = PathBuf::from(input); - self.local_copy_file(&entry, dest_path.as_path()); + let dest_path = PathBuf::from(input); + self.copy_file(entry, dest_path.as_path()); } SelectedFile::Many(entries) => { - // Iter files for (entry, mut dest_path) in entries.into_iter() { dest_path.push(entry.name()); - self.local_copy_file(&entry, dest_path.as_path()); + self.copy_file(entry, dest_path.as_path()); } - // clear selection - self.host_bridge_mut().clear_queue(); - self.reload_host_bridge_filelist(); + // clear selection and reload + self.browser.explorer_mut().clear_queue(); + self.reload_browser_file_list(); } SelectedFile::None => {} } } - /// Copy file on remote - pub(crate) fn action_remote_copy(&mut self, input: String) { - match self.get_remote_selected_entries() { - SelectedFile::One(entry) => { - let dest_path: PathBuf = PathBuf::from(input); - self.remote_copy_file(entry, dest_path.as_path()); - } - SelectedFile::Many(entries) => { - // Iter files - for (entry, mut dest_path) in entries.into_iter() { - dest_path.push(entry.name()); - self.remote_copy_file(entry, dest_path.as_path()); - } - - // clear selection - self.remote_mut().clear_queue(); - self.reload_remote_filelist(); - } - SelectedFile::None => {} - } - } - - fn local_copy_file(&mut self, entry: &File, dest: &Path) { - match self.host_bridge.copy(entry, dest) { + fn copy_file(&mut self, entry: File, dest: &Path) { + match self.browser.fs_pane_mut().fs.copy(&entry, dest) { Ok(_) => { self.log( LogLevel::Info, @@ -66,45 +43,23 @@ impl FileTransferActivity { ), ); } - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not copy \"{}\" to \"{}\": {}", - entry.path().display(), - dest.display(), - err - ), - ), - } - } - - fn remote_copy_file(&mut self, entry: File, dest: &Path) { - match self.client.as_mut().copy(entry.path(), dest) { - Ok(_) => { - self.log( - LogLevel::Info, - format!( - "Copied \"{}\" to \"{}\"", - entry.path().display(), - dest.display() - ), - ); - } - Err(err) => match err.kind { - RemoteErrorType::UnsupportedFeature => { - // If copy is not supported, perform the tricky copy + Err(err) => { + // On remote tabs, fall back to tricky_copy (download + re-upload) + // when the protocol doesn't support server-side copy. + if !self.is_local_tab() { let _ = self.tricky_copy(entry, dest); + } else { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not copy \"{}\" to \"{}\": {}", + entry.path().display(), + dest.display(), + err + ), + ); } - _ => self.log_and_alert( - LogLevel::Error, - format!( - "Could not copy \"{}\" to \"{}\": {}", - entry.path().display(), - dest.display(), - err - ), - ), - }, + } } } @@ -138,7 +93,12 @@ impl FileTransferActivity { return Err(err); } // Stat dir - let tempdir_entry = match self.host_bridge.stat(tempdir_path.as_path()) { + let tempdir_entry = match self + .browser + .local_pane_mut() + .fs + .stat(tempdir_path.as_path()) + { Ok(e) => e, Err(err) => { self.log_and_alert( @@ -191,9 +151,17 @@ impl FileTransferActivity { return Err(err); } // Get local fs entry - let tmpfile_entry = match self.host_bridge.stat(tmpfile.path()) { + let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.path()) { Ok(e) if e.is_file() => e, - Ok(_) => panic!("{} is not a file", tmpfile.path().display()), + Ok(_) => { + let msg = format!( + "Copy failed: \"{}\" is not a file", + tmpfile.path().display() + ); + error!("{msg}"); + self.log_and_alert(LogLevel::Error, msg.clone()); + return Err(msg); + } Err(err) => { self.log_and_alert( LogLevel::Error, diff --git a/src/ui/activities/filetransfer/actions/delete.rs b/src/ui/activities/filetransfer/actions/delete.rs index 56a5bfb..187ec01 100644 --- a/src/ui/activities/filetransfer/actions/delete.rs +++ b/src/ui/activities/filetransfer/actions/delete.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use remotefs::File; @@ -8,72 +8,28 @@ use remotefs::File; use super::{FileTransferActivity, LogLevel, SelectedFile}; impl FileTransferActivity { - pub(crate) fn action_local_delete(&mut self) { - match self.get_local_selected_entries() { + /// Delete the currently selected file(s) via the active tab's pane. + pub(crate) fn action_delete(&mut self) { + match self.get_selected_entries() { SelectedFile::One(entry) => { - // Delete file - self.local_remove_file(&entry); + self.remove_file(&entry); } SelectedFile::Many(entries) => { - // Iter files for (entry, _) in entries.iter() { - // Delete file - self.local_remove_file(entry); + self.remove_file(entry); } - // clear selection - self.host_bridge_mut().clear_queue(); - self.reload_host_bridge_filelist(); + // clear selection and reload + self.browser.explorer_mut().clear_queue(); + self.reload_browser_file_list(); } SelectedFile::None => {} } } - pub(crate) fn action_remote_delete(&mut self) { - match self.get_remote_selected_entries() { - SelectedFile::One(entry) => { - // Delete file - self.remote_remove_file(&entry); - } - SelectedFile::Many(entries) => { - // Iter files - for (entry, _) in entries.iter() { - // Delete file - self.remote_remove_file(entry); - } - - // clear selection - self.remote_mut().clear_queue(); - self.reload_remote_filelist(); - } - SelectedFile::None => {} - } - } - - pub(crate) fn local_remove_file(&mut self, entry: &File) { - match self.host_bridge.remove(entry) { - Ok(_) => { - // Log - self.log( - LogLevel::Info, - format!("Removed file \"{}\"", entry.path().display()), - ); - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not delete file \"{}\": {}", - entry.path().display(), - err - ), - ); - } - } - } - - pub(crate) fn remote_remove_file(&mut self, entry: &File) { - match self.client.remove_dir_all(entry.path()) { + /// Remove a single file or directory via the active tab's pane. + pub(crate) fn remove_file(&mut self, entry: &File) { + match self.browser.fs_pane_mut().fs.remove(entry) { Ok(_) => { self.log( LogLevel::Info, diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index 0dc4a47..82d2803 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use std::fs::OpenOptions; use std::io::Read; @@ -10,6 +10,7 @@ use std::time::SystemTime; use remotefs::File; use remotefs::fs::Metadata; +use super::super::ui_result; use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; impl FileTransferActivity { @@ -28,7 +29,7 @@ impl FileTransferActivity { format!("Opening file \"{}\"…", entry.path().display()), ); // Edit file - let res = match self.host_bridge.is_localhost() { + let res = match self.browser.local_pane().fs.is_localhost() { true => self.edit_local_file(entry.path()).map(|_| ()), false => self.edit_bridged_local_file(entry), }; @@ -88,7 +89,7 @@ impl FileTransferActivity { }; // open from host bridge - let mut reader = match self.host_bridge.open_file(entry.path()) { + let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) { Ok(reader) => reader, Err(err) => { return Err(format!("Failed to open bridged entry: {err}")); @@ -122,7 +123,7 @@ impl FileTransferActivity { return Err(format!("Could not open file: {err}")); } }; - let mut writer = match self.host_bridge.create_file( + let mut writer = match self.browser.local_pane_mut().fs.create_file( entry.path(), &Metadata { size: new_file_size, @@ -139,7 +140,9 @@ impl FileTransferActivity { return Err(format!("Could not write file: {err}")); } - self.host_bridge + self.browser + .local_pane_mut() + .fs .finalize_write(writer) .map_err(|err| format!("Could not write file: {err}"))?; } @@ -177,7 +180,7 @@ impl FileTransferActivity { error!("Could not leave alternate screen: {}", err); } // Lock ports - assert!(self.app.lock_ports().is_ok()); + ui_result(self.app.lock_ports()); // Get current file modification time let prev_mtime = self.get_localhost_mtime(path)?; // Open editor @@ -205,7 +208,7 @@ impl FileTransferActivity { error!("Could not clear screen screen: {}", err); } // Unlock ports - assert!(self.app.unlock_ports().is_ok()); + ui_result(self.app.unlock_ports()); } let after_mtime = self.get_localhost_mtime(path)?; @@ -241,7 +244,8 @@ impl FileTransferActivity { return Err(format!("Could not open file {file_name}: {err}")); } // Get current file modification time - let prev_mtime: SystemTime = match self.host_bridge.stat(tmpfile.as_path()) { + let prev_mtime: SystemTime = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) + { Ok(e) => e.metadata().modified.unwrap_or(std::time::UNIX_EPOCH), Err(err) => { return Err(format!( @@ -254,7 +258,7 @@ impl FileTransferActivity { // Edit file self.edit_local_file(tmpfile.as_path())?; // Get local fs entry - let tmpfile_entry: File = match self.host_bridge.stat(tmpfile.as_path()) { + let tmpfile_entry: File = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( @@ -280,7 +284,7 @@ impl FileTransferActivity { ), ); // Get local fs entry - let tmpfile_entry = match self.host_bridge.stat(tmpfile.as_path()) { + let tmpfile_entry = match self.browser.local_pane_mut().fs.stat(tmpfile.as_path()) { Ok(e) => e, Err(err) => { return Err(format!( diff --git a/src/ui/activities/filetransfer/actions/exec.rs b/src/ui/activities/filetransfer/actions/exec.rs index cdd16d0..0a4b71a 100644 --- a/src/ui/activities/filetransfer/actions/exec.rs +++ b/src/ui/activities/filetransfer/actions/exec.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use std::path::PathBuf; use std::str::FromStr; @@ -37,15 +37,11 @@ impl FromStr for Command { } impl FileTransferActivity { - pub(crate) fn action_local_exec(&mut self, input: String) { - self.action_exec(false, input); + pub(crate) fn action_exec_cmd(&mut self, input: String) { + self.action_exec(input); } - pub(crate) fn action_remote_exec(&mut self, input: String) { - self.action_exec(true, input); - } - - fn action_exec(&mut self, remote: bool, cmd: String) { + fn action_exec(&mut self, cmd: String) { if cmd.is_empty() { self.print_terminal("".to_string()); } @@ -61,10 +57,10 @@ impl FileTransferActivity { match cmd { Command::Cd(path) => { - self.action_exec_cd(remote, path); + self.action_exec_cd(path); } Command::Exec(executable) => { - self.action_exec_executable(remote, executable); + self.action_exec_executable(executable); } Command::Exit => { self.action_exec_exit(); @@ -77,43 +73,20 @@ impl FileTransferActivity { self.umount_exec(); } - fn action_exec_cd(&mut self, remote: bool, input: String) { - let new_dir = if remote { - let dir_path: PathBuf = - self.remote_to_abs_path(PathBuf::from(input.as_str()).as_path()); - self.remote_changedir(dir_path.as_path(), true); - - dir_path - } else { - let dir_path: PathBuf = - self.host_bridge_to_abs_path(PathBuf::from(input.as_str()).as_path()); - self.host_bridge_changedir(dir_path.as_path(), true); - - dir_path - }; + fn action_exec_cd(&mut self, input: String) { + let dir_path = self.pane_to_abs_path(PathBuf::from(input.as_str()).as_path()); + self.pane_changedir(dir_path.as_path(), true); self.update_browser_file_list(); // update prompt and print the new directory self.update_terminal_prompt(); - self.print_terminal(new_dir.display().to_string()); + self.print_terminal(dir_path.display().to_string()); } - /// Execute a [`Command::Exec`] command - fn action_exec_executable(&mut self, remote: bool, cmd: String) { - let res = if remote { - self.client - .as_mut() - .exec(cmd.as_str()) - .map(|(_, output)| output) - .map_err(|e| e.to_string()) - } else { - self.host_bridge - .exec(cmd.as_str()) - .map_err(|e| e.to_string()) - }; - - match res { + /// Execute a command via the active tab's pane. + fn action_exec_executable(&mut self, cmd: String) { + match self.browser.fs_pane_mut().fs.exec(cmd.as_str()) { Ok(output) => { self.print_terminal(output); } @@ -122,7 +95,7 @@ impl FileTransferActivity { LogLevel::Error, format!("Could not execute command \"{cmd}\": {err}"), ); - self.print_terminal(err); + self.print_terminal(err.to_string()); } } } diff --git a/src/ui/activities/filetransfer/actions/file_size.rs b/src/ui/activities/filetransfer/actions/file_size.rs index fb0251e..80bd239 100644 --- a/src/ui/activities/filetransfer/actions/file_size.rs +++ b/src/ui/activities/filetransfer/actions/file_size.rs @@ -1,37 +1,14 @@ use remotefs::File; use super::{FileTransferActivity, LogLevel}; -use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab; - -#[derive(Debug, Copy, Clone)] -enum Host { - HostBridge, - Remote, -} impl FileTransferActivity { + /// Calculate and display the total size of the selected file(s) via the active tab's pane. pub(crate) fn action_get_file_size(&mut self) { - // Get selected file self.mount_blocking_wait("Getting total path size..."); - let total_size = match self.browser.tab() { - FileExplorerTab::HostBridge => { - let files = self.get_local_selected_entries().get_files(); - self.get_files_size(files, Host::HostBridge) - } - FileExplorerTab::Remote => { - let files = self.get_remote_selected_entries().get_files(); - self.get_files_size(files, Host::Remote) - } - FileExplorerTab::FindHostBridge => { - let files = self.get_found_selected_entries().get_files(); - self.get_files_size(files, Host::HostBridge) - } - FileExplorerTab::FindRemote => { - let files = self.get_found_selected_entries().get_files(); - self.get_files_size(files, Host::Remote) - } - }; + let files = self.get_selected_entries().get_files(); + let total_size = self.get_files_size(files); self.umount_wait(); self.mount_info(format!( @@ -40,24 +17,19 @@ impl FileTransferActivity { )); } - fn get_files_size(&mut self, files: Vec, host: Host) -> u64 { - files.into_iter().map(|f| self.get_file_size(f, host)).sum() + fn get_files_size(&mut self, files: Vec) -> u64 { + files.into_iter().map(|f| self.get_file_size(f)).sum() } - fn get_file_size(&mut self, file: File, host: Host) -> u64 { + fn get_file_size(&mut self, file: File) -> u64 { if let Some(symlink) = &file.metadata().symlink { - // stat - let stat_res = match host { - Host::HostBridge => self.host_bridge.stat(symlink).map_err(|e| e.to_string()), - Host::Remote => self.client.stat(symlink).map_err(|e| e.to_string()), - }; - match stat_res { + match self.browser.fs_pane_mut().fs.stat(symlink) { Ok(stat) => stat.metadata().size, - Err(err_msg) => { + Err(err) => { self.log( LogLevel::Error, format!( - "Failed to stat symlink target {path}: {err_msg}", + "Failed to stat symlink target {path}: {err}", path = symlink.display(), ), ); @@ -65,22 +37,13 @@ impl FileTransferActivity { } } } else if file.is_dir() { - // list and sum - let list_res = match host { - Host::HostBridge => self - .host_bridge - .list_dir(&file.path) - .map_err(|e| e.to_string()), - Host::Remote => self.client.list_dir(&file.path).map_err(|e| e.to_string()), - }; - - match list_res { - Ok(list) => list.into_iter().map(|f| self.get_file_size(f, host)).sum(), - Err(err_msg) => { + match self.browser.fs_pane_mut().fs.list_dir(&file.path) { + Ok(list) => list.into_iter().map(|f| self.get_file_size(f)).sum(), + Err(err) => { self.log( LogLevel::Error, format!( - "Failed to list directory {path}: {err_msg}", + "Failed to list directory {path}: {err}", path = file.path.display(), ), ); diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index 9ccff01..f6dec7c 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -1,11 +1,10 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::PathBuf; -use super::super::browser::FileExplorerTab; use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferOpts, TransferPayload}; impl FileTransferActivity { @@ -22,33 +21,19 @@ impl FileTransferActivity { Some(p) => p.to_path_buf(), } }; - // Change directory - match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { - self.host_bridge_changedir(path.as_path(), true) - } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - self.remote_changedir(path.as_path(), true) - } - } + // Change directory on the active tab's pane + self.pane_changedir(path.as_path(), true); } } pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) { - let wrkdir: PathBuf = match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { - self.remote().wrkdir.clone() - } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - self.host_bridge().wrkdir.clone() - } - }; + let wrkdir: PathBuf = self.browser.opposite_pane().explorer.wrkdir.clone(); match self.get_found_selected_entries() { - SelectedFile::One(entry) => match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { + SelectedFile::One(entry) => { + if self.is_local_tab() { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.remote_file_exists(file_to_check.as_path()) + && self.file_exists(file_to_check.as_path(), false) && !self.should_replace_file( opts.save_as.clone().unwrap_or_else(|| entry.name()), ) @@ -66,11 +51,10 @@ impl FileTransferActivity { format!("Could not upload file: {err}"), ); } - } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { + } else { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.host_bridge_file_exists(file_to_check.as_path()) + && self.file_exists(file_to_check.as_path(), true) && !self.should_replace_file( opts.save_as.clone().unwrap_or_else(|| entry.name()), ) @@ -89,7 +73,7 @@ impl FileTransferActivity { ); } } - }, + } SelectedFile::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; @@ -97,58 +81,51 @@ impl FileTransferActivity { dest_path.push(save_as); } // Iter files - match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { - let super::save::TransferFilesWithOverwritesResult::FilesToTransfer( - entries, - ) = self.get_files_to_transfer_with_overwrites( + if self.is_local_tab() { + let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) = + self.get_files_to_transfer_with_overwrites( entries, super::save::CheckFileExists::Remote, ) - else { - debug!("User cancelled file transfer due to overwrites"); - return; - }; - if let Err(err) = self.filetransfer_send( - TransferPayload::TransferQueue(entries), - dest_path.as_path(), - None, - ) { - { - self.log_and_alert( - LogLevel::Error, - format!("Could not upload file: {err}"), - ); - } - } + else { + debug!("User cancelled file transfer due to overwrites"); + return; + }; + if let Err(err) = self.filetransfer_send( + TransferPayload::TransferQueue(entries), + dest_path.as_path(), + None, + ) { + self.log_and_alert( + LogLevel::Error, + format!("Could not upload file: {err}"), + ); } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - let super::save::TransferFilesWithOverwritesResult::FilesToTransfer( - entries, - ) = self.get_files_to_transfer_with_overwrites( + } else { + let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(entries) = + self.get_files_to_transfer_with_overwrites( entries, super::save::CheckFileExists::HostBridge, ) - else { - debug!("User cancelled file transfer due to overwrites"); - return; - }; - if let Err(err) = self.filetransfer_recv( - TransferPayload::TransferQueue(entries), - dest_path.as_path(), - None, - ) { - self.log_and_alert( - LogLevel::Error, - format!("Could not download file: {err}"), - ); - } + else { + debug!("User cancelled file transfer due to overwrites"); + return; + }; + if let Err(err) = self.filetransfer_recv( + TransferPayload::TransferQueue(entries), + dest_path.as_path(), + None, + ) { + self.log_and_alert( + LogLevel::Error, + format!("Could not download file: {err}"), + ); + } - // clear selection - if let Some(f) = self.found_mut() { - f.clear_queue(); - self.update_find_list(); - } + // clear selection + if let Some(f) = self.found_mut() { + f.clear_queue(); + self.update_find_list(); } } } @@ -159,14 +136,11 @@ impl FileTransferActivity { pub(crate) fn action_find_delete(&mut self) { match self.get_found_selected_entries() { SelectedFile::One(entry) => { - // Delete file - self.remove_found_file(&entry); + self.remove_file(&entry); } SelectedFile::Many(entries) => { - // Iter files for (entry, _) in entries.iter() { - // Delete file - self.remove_found_file(entry); + self.remove_file(entry); } // clear selection @@ -179,17 +153,6 @@ impl FileTransferActivity { } } - fn remove_found_file(&mut self, entry: &File) { - match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { - self.local_remove_file(entry); - } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - self.remote_remove_file(entry); - } - } - } - pub(crate) fn action_find_open(&mut self) { match self.get_found_selected_entries() { SelectedFile::One(entry) => { @@ -235,13 +198,6 @@ impl FileTransferActivity { } fn open_found_file(&mut self, entry: &File, with: Option<&str>) { - match self.browser.tab() { - FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => { - self.action_open_local_file(entry, with); - } - FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - self.action_open_remote_file(entry, with); - } - } + self.open_file(entry, with); } } diff --git a/src/ui/activities/filetransfer/actions/mark.rs b/src/ui/activities/filetransfer/actions/mark.rs index d126718..e76e732 100644 --- a/src/ui/activities/filetransfer/actions/mark.rs +++ b/src/ui/activities/filetransfer/actions/mark.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use super::FileTransferActivity; diff --git a/src/ui/activities/filetransfer/actions/mkdir.rs b/src/ui/activities/filetransfer/actions/mkdir.rs index 2350c58..602eb0e 100644 --- a/src/ui/activities/filetransfer/actions/mkdir.rs +++ b/src/ui/activities/filetransfer/actions/mkdir.rs @@ -1,49 +1,46 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::PathBuf; -use remotefs::fs::UnixPex; - use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - pub(crate) fn action_local_mkdir(&mut self, input: String) { - match self - .host_bridge - .mkdir(PathBuf::from(input.as_str()).as_path()) - { - Ok(_) => { - // Reload files - self.log(LogLevel::Info, format!("Created directory \"{input}\"")); - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not create directory \"{input}\": {err}"), - ); - } + /// Create a directory via the active tab's pane. + pub(crate) fn action_mkdir(&mut self, input: String) { + let path = PathBuf::from(input.as_str()); + match self.browser.fs_pane_mut().fs.mkdir(path.as_path()) { + Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")), + Err(err) => self.log_and_alert( + LogLevel::Error, + format!("Could not create directory \"{input}\": {err}"), + ), } } - pub(crate) fn action_remote_mkdir(&mut self, input: String) { - match self.client.as_mut().create_dir( - PathBuf::from(input.as_str()).as_path(), - UnixPex::from(0o755), - ) { - Ok(_) => { - // Reload files - self.log(LogLevel::Info, format!("Created directory \"{input}\"")); - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not create directory \"{input}\": {err}"), - ); - } + + /// Create a directory on the local host (used by sync-browsing in change_dir). + pub(in crate::ui::activities::filetransfer) fn action_local_mkdir(&mut self, input: String) { + let path = PathBuf::from(input.as_str()); + match self.browser.local_pane_mut().fs.mkdir(path.as_path()) { + Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")), + Err(err) => self.log_and_alert( + LogLevel::Error, + format!("Could not create directory \"{input}\": {err}"), + ), + } + } + + /// Create a directory on the remote host (used by sync-browsing in change_dir). + pub(in crate::ui::activities::filetransfer) fn action_remote_mkdir(&mut self, input: String) { + let path = PathBuf::from(input.as_str()); + match self.browser.remote_pane_mut().fs.mkdir(path.as_path()) { + Ok(_) => self.log(LogLevel::Info, format!("Created directory \"{input}\"")), + Err(err) => self.log_and_alert( + LogLevel::Error, + format!("Could not create directory \"{input}\": {err}"), + ), } } } diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 600a0b0..222e7bf 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use std::path::{Path, PathBuf}; @@ -92,36 +92,43 @@ impl FileTransferActivity { self.get_selected_files(&Id::ExplorerHostBridge) } - pub(crate) fn get_local_selected_file(&self) -> Option { - self.get_selected_file(&Id::ExplorerHostBridge) - } - /// Get remote file entry pub(crate) fn get_remote_selected_entries(&mut self) -> SelectedFile { self.get_selected_files(&Id::ExplorerRemote) } - pub(crate) fn get_remote_selected_file(&self) -> Option { - self.get_selected_file(&Id::ExplorerRemote) - } - - /// Returns whether only one entry is selected on local host - pub(crate) fn is_local_selected_one(&mut self) -> bool { - matches!(self.get_local_selected_entries(), SelectedFile::One(_)) - } - - /// Returns whether only one entry is selected on remote host - pub(crate) fn is_remote_selected_one(&mut self) -> bool { - matches!(self.get_remote_selected_entries(), SelectedFile::One(_)) - } - /// Get remote file entry pub(crate) fn get_found_selected_entries(&mut self) -> SelectedFile { self.get_selected_files(&Id::ExplorerFind) } pub(crate) fn get_found_selected_file(&self) -> Option { - self.get_selected_file(&Id::ExplorerFind) + self.get_selected_file_by_id(&Id::ExplorerFind) + } + + /// Get selected entries from whichever tab is currently active. + pub(crate) fn get_selected_entries(&mut self) -> SelectedFile { + let id = match self.browser.tab() { + FileExplorerTab::HostBridge => Id::ExplorerHostBridge, + FileExplorerTab::Remote => Id::ExplorerRemote, + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind, + }; + self.get_selected_files(&id) + } + + /// Get single selected file from whichever tab is currently active. + pub(crate) fn get_selected_file(&self) -> Option { + let id = match self.browser.tab() { + FileExplorerTab::HostBridge => Id::ExplorerHostBridge, + FileExplorerTab::Remote => Id::ExplorerRemote, + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => Id::ExplorerFind, + }; + self.get_selected_file_by_id(&id) + } + + /// Returns whether only one entry is selected on the current tab. + pub(crate) fn is_selected_one(&mut self) -> bool { + matches!(self.get_selected_entries(), SelectedFile::One(_)) } // -- private @@ -164,13 +171,13 @@ impl FileTransferActivity { fn get_file_from_path(&mut self, id: &Id, path: &Path) -> Option { match *id { - Id::ExplorerHostBridge => self.host_bridge.stat(path).ok(), - Id::ExplorerRemote => self.client.stat(path).ok(), + Id::ExplorerHostBridge => self.browser.local_pane_mut().fs.stat(path).ok(), + Id::ExplorerRemote => self.browser.remote_pane_mut().fs.stat(path).ok(), Id::ExplorerFind => { let found = self.browser.found_tab().unwrap(); match found { - FoundExplorerTab::Local => self.host_bridge.stat(path).ok(), - FoundExplorerTab::Remote => self.client.stat(path).ok(), + FoundExplorerTab::Local => self.browser.local_pane_mut().fs.stat(path).ok(), + FoundExplorerTab::Remote => self.browser.remote_pane_mut().fs.stat(path).ok(), } } _ => None, @@ -182,11 +189,16 @@ impl FileTransferActivity { Id::ExplorerHostBridge => self.host_bridge(), Id::ExplorerRemote => self.remote(), Id::ExplorerFind => self.found().as_ref().unwrap(), - _ => unreachable!(), + _ => { + error!( + "browser_by_id called with unexpected id: {id:?}; falling back to local explorer" + ); + self.host_bridge() + } } } - fn get_selected_file(&self, id: &Id) -> Option { + fn get_selected_file_by_id(&self, id: &Id) -> Option { let browser = self.browser_by_id(id); // if no transfer queue, return selected files match self.get_selected_index(id) { diff --git a/src/ui/activities/filetransfer/actions/newfile.rs b/src/ui/activities/filetransfer/actions/newfile.rs index 41b4c50..09543e2 100644 --- a/src/ui/activities/filetransfer/actions/newfile.rs +++ b/src/ui/activities/filetransfer/actions/newfile.rs @@ -1,33 +1,33 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals -use std::fs::File as StdFile; use std::path::PathBuf; use remotefs::fs::Metadata; -use super::{File, FileTransferActivity, LogLevel}; +use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - pub(crate) fn action_local_newfile(&mut self, input: String) { - // Check if file exists - let mut file_exists: bool = false; - for file in self.host_bridge().iter_files_all() { - if input == file.name() { - file_exists = true; - } - } - if file_exists { - self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); + /// Create a new empty file via the active tab's pane. + pub(crate) fn action_newfile(&mut self, input: String) { + // Check if file exists in current explorer listing + if self + .browser + .explorer() + .iter_files_all() + .any(|file| input == file.name()) + { + self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists")); return; } - // Create file - let file_path: PathBuf = PathBuf::from(input.as_str()); + let file_path = PathBuf::from(input.as_str()); let writer = match self - .host_bridge + .browser + .fs_pane_mut() + .fs .create_file(file_path.as_path(), &Metadata::default()) { Ok(f) => f, @@ -40,7 +40,7 @@ impl FileTransferActivity { } }; // finalize write - if let Err(err) = self.host_bridge.finalize_write(writer) { + if let Err(err) = self.browser.fs_pane_mut().fs.finalize_write(writer) { self.log_and_alert( LogLevel::Error, format!("Could not write file \"{}\": {}", file_path.display(), err), @@ -53,67 +53,4 @@ impl FileTransferActivity { format!("Created file \"{}\"", file_path.display()), ); } - - pub(crate) fn action_remote_newfile(&mut self, input: String) { - // Check if file exists - let mut file_exists: bool = false; - for file in self.remote().iter_files_all() { - if input == file.name() { - file_exists = true; - } - } - if file_exists { - self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); - return; - } - // Get path on remote - let file_path: PathBuf = PathBuf::from(input.as_str()); - // Create file (on local) - match tempfile::NamedTempFile::new() { - Err(err) => { - self.log_and_alert(LogLevel::Error, format!("Could not create tempfile: {err}")) - } - Ok(tfile) => { - // Stat tempfile - let local_file: File = match self.host_bridge.stat(tfile.path()) { - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not stat tempfile: {err}"), - ); - return; - } - Ok(f) => f, - }; - if local_file.is_file() { - // Create file - let reader = Box::new(match StdFile::open(tfile.path()) { - Ok(f) => f, - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not open tempfile: {err}"), - ); - return; - } - }); - match self - .client - .create_file(file_path.as_path(), &local_file.metadata, reader) - { - Err(err) => self.log_and_alert( - LogLevel::Error, - format!("Could not create file \"{}\": {}", file_path.display(), err), - ), - Ok(_) => { - self.log( - LogLevel::Info, - format!("Created file \"{}\"", file_path.display()), - ); - } - } - } - } - } - } } diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index 05b8b26..9f91a92 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -1,57 +1,50 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals // ext use std::path::{Path, PathBuf}; -use super::{File, FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; +use super::{File, FileTransferActivity, LogLevel, TransferPayload}; impl FileTransferActivity { - /// Open local file - pub(crate) fn action_open_local(&mut self) { - let entries: Vec = match self.get_local_selected_entries() { - SelectedFile::One(entry) => vec![entry], - SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(), - SelectedFile::None => vec![], - }; - entries - .iter() - .for_each(|x| self.action_open_local_file(x, None)); + /// Open selected file(s) with default application + pub(crate) fn action_open(&mut self) { + let entries = self.get_selected_entries().get_files(); + entries.iter().for_each(|x| self.open_file(x, None)); - // clear selection - self.host_bridge_mut().clear_queue(); - self.reload_host_bridge_filelist(); + // clear queue + self.browser.explorer_mut().clear_queue(); + self.reload_browser_file_list(); } - /// Open local file - pub(crate) fn action_open_remote(&mut self) { - let entries: Vec = match self.get_remote_selected_entries() { - SelectedFile::One(entry) => vec![entry], - SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(), - SelectedFile::None => vec![], - }; - entries - .iter() - .for_each(|x| self.action_open_remote_file(x, None)); + /// Open selected file(s) with provided application + pub(crate) fn action_open_with(&mut self, with: &str) { + let entries = self.get_selected_entries().get_files(); + entries.iter().for_each(|x| self.open_file(x, Some(with))); - // clear selection - self.remote_mut().clear_queue(); - self.reload_remote_filelist(); + // clear queue + self.browser.explorer_mut().clear_queue(); + self.reload_browser_file_list(); } - /// Perform open lopcal file - pub(crate) fn action_open_local_file(&mut self, entry: &File, open_with: Option<&str>) { - if self.host_bridge.is_localhost() { + /// Open a file, dispatching based on whether the active pane is localhost. + pub(crate) fn open_file(&mut self, entry: &File, open_with: Option<&str>) { + if self.browser.fs_pane().fs.is_localhost() { + // Direct open from local path self.open_path_with(entry.path(), open_with); - } else { + } else if self.is_local_tab() { + // Non-localhost host bridge: download via HostBridge API self.open_bridged_file(entry, open_with); + } else { + // Remote: download via filetransfer_recv + self.action_open_remote_file(entry, open_with); } } /// Open remote file. The file is first downloaded to a temporary directory on localhost - pub(crate) fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) { + fn action_open_remote_file(&mut self, entry: &File, open_with: Option<&str>) { // Download file let tmpfile: String = match self.get_cache_tmp_name(&entry.name(), entry.extension().as_deref()) { @@ -90,39 +83,6 @@ impl FileTransferActivity { } } - /// Open selected file with provided application - pub(crate) fn action_local_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_local_selected_entries() { - SelectedFile::One(entry) => vec![entry], - SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(), - SelectedFile::None => vec![], - }; - // Open all entries - entries - .iter() - .for_each(|x| self.action_open_local_file(x, Some(with))); - - // clear selection - self.host_bridge_mut().clear_queue(); - } - - /// Open selected file with provided application - pub(crate) fn action_remote_open_with(&mut self, with: &str) { - let entries: Vec = match self.get_remote_selected_entries() { - SelectedFile::One(entry) => vec![entry], - SelectedFile::Many(entries) => entries.into_iter().map(|(f, _)| f).collect(), - SelectedFile::None => vec![], - }; - // Open all entries - entries - .iter() - .for_each(|x| self.action_open_remote_file(x, Some(with))); - - // clear selection - self.remote_mut().clear_queue(); - self.reload_remote_filelist(); - } - fn open_bridged_file(&mut self, entry: &File, open_with: Option<&str>) { // Download file let tmpfile: String = @@ -144,7 +104,7 @@ impl FileTransferActivity { let tmpfile = cache.join(tmpfile); // open from host bridge - let mut reader = match self.host_bridge.open_file(entry.path()) { + let mut reader = match self.browser.local_pane_mut().fs.open_file(entry.path()) { Ok(reader) => reader, Err(err) => { self.log( diff --git a/src/ui/activities/filetransfer/actions/rename.rs b/src/ui/activities/filetransfer/actions/rename.rs index c9109d5..25e8c7e 100644 --- a/src/ui/activities/filetransfer/actions/rename.rs +++ b/src/ui/activities/filetransfer/actions/rename.rs @@ -1,62 +1,38 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::{Path, PathBuf}; -use remotefs::RemoteErrorType; - use super::{File, FileTransferActivity, LogLevel, SelectedFile}; impl FileTransferActivity { - pub(crate) fn action_local_rename(&mut self, input: String) { - match self.get_local_selected_entries() { + /// Rename / move the currently selected entries via the active tab's pane. + pub(crate) fn action_rename(&mut self, input: String) { + match self.get_selected_entries() { SelectedFile::One(entry) => { - let dest_path: PathBuf = PathBuf::from(input); - self.local_rename_file(&entry, dest_path.as_path()); + let dest_path = PathBuf::from(input); + self.rename_file(&entry, dest_path.as_path()); } SelectedFile::Many(entries) => { - // Try to copy each file to Input/{FILE_NAME} - // Iter files for (entry, mut dest_path) in entries.into_iter() { dest_path.push(entry.name()); - self.local_rename_file(&entry, dest_path.as_path()); + self.rename_file(&entry, dest_path.as_path()); } - // clear selection - self.host_bridge_mut().clear_queue(); - self.reload_host_bridge_filelist(); + // clear selection and reload + self.browser.explorer_mut().clear_queue(); + self.reload_browser_file_list(); } SelectedFile::None => {} } } - pub(crate) fn action_remote_rename(&mut self, input: String) { - match self.get_remote_selected_entries() { - SelectedFile::One(entry) => { - let dest_path: PathBuf = PathBuf::from(input); - self.remote_rename_file(&entry, dest_path.as_path()); - } - SelectedFile::Many(entries) => { - // Try to copy each file to Input/{FILE_NAME} - // Iter files - for (entry, mut dest_path) in entries.into_iter() { - dest_path.push(entry.name()); - self.remote_rename_file(&entry, dest_path.as_path()); - } - - // clear selection - self.remote_mut().clear_queue(); - // reload remote - self.reload_remote_filelist(); - } - SelectedFile::None => {} - } - } - - fn local_rename_file(&mut self, entry: &File, dest: &Path) { - match self.host_bridge.rename(entry, dest) { + /// Rename a single file via the active tab's pane. + /// Falls back to `tricky_move` on remote tabs when rename fails. + fn rename_file(&mut self, entry: &File, dest: &Path) { + match self.browser.fs_pane_mut().fs.rename(entry, dest) { Ok(_) => { self.log( LogLevel::Info, @@ -67,20 +43,31 @@ impl FileTransferActivity { ), ); } - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not move \"{}\" to \"{}\": {}", - entry.path().display(), - dest.display(), - err - ), - ), + Err(err) => { + if !self.is_local_tab() { + // Try tricky_move as a fallback on remote + debug!("Rename failed ({err}); attempting tricky_move"); + self.tricky_move(entry, dest); + } else { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not move \"{}\" to \"{}\": {}", + entry.path().display(), + dest.display(), + err + ), + ); + } + } } } + /// Rename / move a file on the remote host. + /// Falls back to `tricky_move` when the rename fails. + /// Also used by fswatcher for syncing renames. pub(crate) fn remote_rename_file(&mut self, entry: &File, dest: &Path) { - match self.client.as_mut().mov(entry.path(), dest) { + match self.browser.remote_pane_mut().fs.rename(entry, dest) { Ok(_) => { self.log( LogLevel::Info, @@ -91,18 +78,11 @@ impl FileTransferActivity { ), ); } - Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { + Err(err) => { + // Try tricky_move as a fallback + debug!("Rename failed ({err}); attempting tricky_move"); self.tricky_move(entry, dest); } - Err(err) => self.log_and_alert( - LogLevel::Error, - format!( - "Could not move \"{}\" to \"{}\": {}", - entry.path().display(), - dest.display(), - err - ), - ), } } @@ -117,7 +97,7 @@ impl FileTransferActivity { if self.tricky_copy(entry.clone(), dest).is_ok() { // Delete remote existing entry debug!("Tricky-copy worked; removing existing remote entry"); - match self.client.remove_dir_all(entry.path()) { + match self.browser.remote_pane_mut().fs.remove(entry) { Ok(_) => self.log( LogLevel::Info, format!( diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index d6c477e..b20461d 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::{Path, PathBuf}; @@ -42,20 +42,20 @@ enum AllOpts { } impl FileTransferActivity { - pub(crate) fn action_local_saveas(&mut self, input: String) { - self.local_send_file(TransferOpts::default().save_as(Some(input))); + pub(crate) fn action_saveas(&mut self, input: String) { + if self.is_local_tab() { + self.local_send_file(TransferOpts::default().save_as(Some(input))); + } else { + self.remote_recv_file(TransferOpts::default().save_as(Some(input))); + } } - pub(crate) fn action_remote_saveas(&mut self, input: String) { - self.remote_recv_file(TransferOpts::default().save_as(Some(input))); - } - - pub(crate) fn action_local_send(&mut self) { - self.local_send_file(TransferOpts::default()); - } - - pub(crate) fn action_remote_recv(&mut self) { - self.remote_recv_file(TransferOpts::default()); + pub(crate) fn action_transfer_file(&mut self) { + if self.is_local_tab() { + self.local_send_file(TransferOpts::default()); + } else { + self.remote_recv_file(TransferOpts::default()); + } } fn local_send_file(&mut self, opts: TransferOpts) { @@ -64,7 +64,7 @@ impl FileTransferActivity { SelectedFile::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.remote_file_exists(file_to_check.as_path()) + && self.file_exists(file_to_check.as_path(), false) && !self .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) { @@ -124,7 +124,7 @@ impl FileTransferActivity { SelectedFile::One(entry) => { let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); if self.config().get_prompt_on_file_replace() - && self.host_bridge_file_exists(file_to_check.as_path()) + && self.file_exists(file_to_check.as_path(), true) && !self .should_replace_file(opts.save_as.clone().unwrap_or_else(|| entry.name())) { @@ -307,8 +307,8 @@ impl FileTransferActivity { files.into_iter().partition(|(x, dest_path)| { let p = Self::file_to_check_many(x, dest_path); match file_exists { - CheckFileExists::Remote => self.remote_file_exists(p.as_path()), - CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()), + CheckFileExists::Remote => self.file_exists(p.as_path(), false), + CheckFileExists::HostBridge => self.file_exists(p.as_path(), true), } }); diff --git a/src/ui/activities/filetransfer/actions/scan.rs b/src/ui/activities/filetransfer/actions/scan.rs index 9cb052b..67f6cc4 100644 --- a/src/ui/activities/filetransfer/actions/scan.rs +++ b/src/ui/activities/filetransfer/actions/scan.rs @@ -1,19 +1,14 @@ use std::path::Path; use super::{File, FileTransferActivity}; -use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab; impl FileTransferActivity { + /// List directory contents via the active tab's pane. pub(crate) fn action_scan(&mut self, p: &Path) -> Result, String> { - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self - .host_bridge - .list_dir(p) - .map_err(|e| format!("Failed to list directory: {}", e)), - FileExplorerTab::Remote | FileExplorerTab::FindRemote => self - .client - .list_dir(p) - .map_err(|e| format!("Failed to list directory: {}", e)), - } + self.browser + .fs_pane_mut() + .fs + .list_dir(p) + .map_err(|e| format!("Failed to list directory: {}", e)) } } diff --git a/src/ui/activities/filetransfer/actions/submit.rs b/src/ui/activities/filetransfer/actions/submit.rs index ccead4a..9fea45d 100644 --- a/src/ui/activities/filetransfer/actions/submit.rs +++ b/src/ui/activities/filetransfer/actions/submit.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use super::{File, FileTransferActivity}; @@ -11,16 +11,15 @@ enum SubmitAction { } impl FileTransferActivity { - /// Decides which action to perform on submit for local explorer - /// Return true whether the directory changed - pub(crate) fn action_submit_local(&mut self, entry: File) { + /// Decides which action to perform on submit, dispatching via the active tab's pane. + pub(crate) fn action_submit(&mut self, entry: File) { let (action, entry) = if entry.is_dir() { (SubmitAction::ChangeDir, entry) } else if entry.metadata().symlink.is_some() { - // Stat file + // Stat symlink target via the active pane let symlink = entry.metadata().symlink.as_ref().unwrap(); - let stat_file = match self.host_bridge.stat(symlink.as_path()) { - Ok(e) => e, + match self.browser.fs_pane_mut().fs.stat(symlink.as_path()) { + Ok(e) => (SubmitAction::ChangeDir, e), Err(err) => { warn!( "Could not stat file pointed by {} ({}): {}", @@ -28,44 +27,14 @@ impl FileTransferActivity { symlink.display(), err ); - entry + (SubmitAction::ChangeDir, entry) } - }; - (SubmitAction::ChangeDir, stat_file) + } } else { (SubmitAction::None, entry) }; if let (SubmitAction::ChangeDir, entry) = (action, entry) { - self.action_enter_local_dir(entry) - } - } - - /// Decides which action to perform on submit for remote explorer - /// Return true whether the directory changed - pub(crate) fn action_submit_remote(&mut self, entry: File) { - let (action, entry) = if entry.is_dir() { - (SubmitAction::ChangeDir, entry) - } else if entry.metadata().symlink.is_some() { - // Stat file - let symlink = entry.metadata().symlink.as_ref().unwrap(); - let stat_file = match self.client.stat(symlink.as_path()) { - Ok(e) => e, - Err(err) => { - warn!( - "Could not stat file pointed by {} ({}): {}", - entry.path().display(), - symlink.display(), - err - ); - entry - } - }; - (SubmitAction::ChangeDir, stat_file) - } else { - (SubmitAction::None, entry) - }; - if let (SubmitAction::ChangeDir, entry) = (action, entry) { - self.action_enter_remote_dir(entry) + self.action_enter_dir(entry) } } } diff --git a/src/ui/activities/filetransfer/actions/symlink.rs b/src/ui/activities/filetransfer/actions/symlink.rs index 75c0dab..f02f701 100644 --- a/src/ui/activities/filetransfer/actions/symlink.rs +++ b/src/ui/activities/filetransfer/actions/symlink.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::PathBuf; @@ -8,57 +8,39 @@ use std::path::PathBuf; use super::{FileTransferActivity, LogLevel}; impl FileTransferActivity { - /// Create symlink on localhost - pub(crate) fn action_local_symlink(&mut self, name: String) { - if let Some(entry) = self.get_local_selected_file() { - match self - .host_bridge - .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) - { - Ok(_) => { - self.log( - LogLevel::Info, - format!( - "Created symlink at {}, pointing to {}", - name, - entry.path().display() - ), - ); - } - Err(err) => { - self.log_and_alert(LogLevel::Error, format!("Could not create symlink: {err}")); - } + /// Create a symlink for the currently selected file via the active tab's pane. + pub(crate) fn action_symlink(&mut self, name: String) { + let entry = if let Some(e) = self.get_selected_file() { + e + } else { + return; + }; + let link_path = PathBuf::from(name.as_str()); + match self + .browser + .fs_pane_mut() + .fs + .symlink(link_path.as_path(), entry.path()) + { + Ok(_) => { + self.log( + LogLevel::Info, + format!( + "Created symlink at {}, pointing to {}", + name, + entry.path().display() + ), + ); } - } - } - - /// Copy file on remote - pub(crate) fn action_remote_symlink(&mut self, name: String) { - if let Some(entry) = self.get_remote_selected_file() { - match self - .client - .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) - { - Ok(_) => { - self.log( - LogLevel::Info, - format!( - "Created symlink at {}, pointing to {}", - name, - entry.path().display() - ), - ); - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not create symlink pointing to {}: {}", - entry.path().display(), - err - ), - ); - } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not create symlink pointing to {}: {}", + entry.path().display(), + err + ), + ); } } } diff --git a/src/ui/activities/filetransfer/actions/walkdir.rs b/src/ui/activities/filetransfer/actions/walkdir.rs index 7716cc2..926bbd7 100644 --- a/src/ui/activities/filetransfer/actions/walkdir.rs +++ b/src/ui/activities/filetransfer/actions/walkdir.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals use std::path::{Path, PathBuf}; @@ -15,17 +15,22 @@ pub enum WalkdirError { } impl FileTransferActivity { - pub(crate) fn action_walkdir_local(&mut self) -> Result, WalkdirError> { + /// Walk the directory tree from the current working directory of the active pane. + pub(crate) fn action_walkdir(&mut self) -> Result, WalkdirError> { let mut acc = Vec::with_capacity(32_768); let pwd = self - .host_bridge + .browser + .fs_pane_mut() + .fs .pwd() .map_err(|e| WalkdirError::Error(e.to_string()))?; self.walkdir(&mut acc, &pwd, |activity, path| { activity - .host_bridge + .browser + .fs_pane_mut() + .fs .list_dir(path) .map_err(|e| e.to_string()) })?; @@ -33,21 +38,6 @@ impl FileTransferActivity { Ok(acc) } - pub(crate) fn action_walkdir_remote(&mut self) -> Result, WalkdirError> { - let mut acc = Vec::with_capacity(32_768); - - let pwd = self - .client - .pwd() - .map_err(|e| WalkdirError::Error(e.to_string()))?; - - self.walkdir(&mut acc, &pwd, |activity, path| { - activity.client.list_dir(path).map_err(|e| e.to_string()) - })?; - - Ok(acc) - } - fn walkdir( &mut self, acc: &mut Vec, diff --git a/src/ui/activities/filetransfer/components/mod.rs b/src/ui/activities/filetransfer/components/mod.rs index 4f20a1e..930399b 100644 --- a/src/ui/activities/filetransfer/components/mod.rs +++ b/src/ui/activities/filetransfer/components/mod.rs @@ -6,7 +6,7 @@ use tui_realm_stdlib::Phantom; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers}; use tuirealm::{Component, MockComponent, NoUserEvent}; -use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; +use super::{Msg, TransferMsg, UiMsg}; // -- export mod log; diff --git a/src/ui/activities/filetransfer/components/popups.rs b/src/ui/activities/filetransfer/components/popups.rs index 2c5469a..e9b70f3 100644 --- a/src/ui/activities/filetransfer/components/popups.rs +++ b/src/ui/activities/filetransfer/components/popups.rs @@ -3,1866 +3,47 @@ //! popups components mod chmod; +mod copy; +mod delete; +mod disconnect; +mod error; +mod file_info; +mod filter; mod goto; - -use std::time::UNIX_EPOCH; - -use bytesize::ByteSize; -use remotefs::File; -use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; -use tuirealm::command::{Cmd, CmdResult, Direction, Position}; -use tuirealm::event::{Key, KeyEvent, KeyModifiers}; -use tuirealm::props::{ - Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TableBuilder, TextSpan, -}; -use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; -#[cfg(posix)] -use uzers::{get_group_by_gid, get_user_by_uid}; +mod keybindings; +mod mkdir; +mod newfile; +mod open_with; +mod progress_bar; +mod quit; +mod rename; +mod replace; +mod save_as; +mod sorting; +mod status_bar; +mod symlink; +mod wait; +mod watcher; pub use self::chmod::ChmodPopup; +pub use self::copy::CopyPopup; +pub use self::delete::DeletePopup; +pub use self::disconnect::DisconnectPopup; +pub use self::error::{ErrorPopup, FatalPopup}; +pub use self::file_info::FileInfoPopup; +pub use self::filter::FilterPopup; pub use self::goto::{ATTR_FILES, GotoPopup}; -use super::super::Browser; -use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; -use crate::explorer::FileSorting; -use crate::utils::fmt::fmt_time; - -#[derive(MockComponent)] -pub struct CopyPopup { - component: Input, -} - -impl CopyPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "destination", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Copy file(s) to…", Alignment::Center), - } - } -} - -impl Component for CopyPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => { - Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) - } - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseCopyPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct FilterPopup { - component: Input, -} - -impl FilterPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "regex or wildmatch", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title( - "Filter files by regex or wildmatch in the current directory", - Alignment::Center, - ), - } - } -} - -impl Component for FilterPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))), - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseFilterPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct DeletePopup { - component: Radio, -} - -impl DeletePopup { - pub fn new(color: Color) -> Self { - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Yes", "No"]) - .value(1) - .title("Delete file(s)?", Alignment::Center), - } - } -} - -impl Component for DeletePopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseDeletePopup)) - } - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Transfer(TransferMsg::DeleteFile)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::CloseDeletePopup)), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - if matches!( - self.perform(Cmd::Submit), - CmdResult::Submit(State::One(StateValue::Usize(0))) - ) { - Some(Msg::Transfer(TransferMsg::DeleteFile)) - } else { - Some(Msg::Ui(UiMsg::CloseDeletePopup)) - } - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct DisconnectPopup { - component: Radio, -} - -impl DisconnectPopup { - pub fn new(color: Color) -> Self { - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Yes", "No"]) - .title("Are you sure you want to disconnect?", Alignment::Center), - } - } -} - -impl Component for DisconnectPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) - } - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::Disconnect)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::CloseDisconnectPopup)), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - if matches!( - self.perform(Cmd::Submit), - CmdResult::Submit(State::One(StateValue::Usize(0))) - ) { - Some(Msg::Ui(UiMsg::Disconnect)) - } else { - Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) - } - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct ErrorPopup { - component: Paragraph, -} - -impl ErrorPopup { - pub fn new>(text: S, color: Color) -> Self { - Self { - component: Paragraph::default() - .alignment(Alignment::Center) - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .text([TextSpan::from(text.as_ref())]) - .wrap(true), - } - } -} - -impl Component for ErrorPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Esc | Key::Enter, - .. - }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct FatalPopup { - component: Paragraph, -} - -impl FatalPopup { - pub fn new>(text: S, color: Color) -> Self { - Self { - component: Paragraph::default() - .alignment(Alignment::Center) - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .text([TextSpan::from(text.as_ref())]) - .wrap(true), - } - } -} - -impl Component for FatalPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Esc | Key::Enter, - .. - }) => Some(Msg::Ui(UiMsg::CloseFatalPopup)), - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct FileInfoPopup { - component: List, -} - -impl FileInfoPopup { - pub fn new(file: &File) -> Self { - let mut texts: TableBuilder = TableBuilder::default(); - // Abs path - let real_path = file.metadata().symlink.as_deref(); - let path: String = match real_path { - Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()), - None => format!("{}", file.path().display()), - }; - // Make texts - texts - .add_col(TextSpan::from("Path: ")) - .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); - texts - .add_row() - .add_col(TextSpan::from("Name: ")) - .add_col(TextSpan::new(file.name()).fg(Color::Yellow)); - if let Some(filetype) = file.extension() { - texts - .add_row() - .add_col(TextSpan::from("File type: ")) - .add_col(TextSpan::new(filetype).fg(Color::LightGreen)); - } - let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size); - texts - .add_row() - .add_col(TextSpan::from("Size: ")) - .add_col(TextSpan::new(format!("{bsize} ({size})").as_str()).fg(Color::Cyan)); - let atime: String = fmt_time( - file.metadata().accessed.unwrap_or(UNIX_EPOCH), - "%b %d %Y %H:%M:%S", - ); - let ctime: String = fmt_time( - file.metadata().created.unwrap_or(UNIX_EPOCH), - "%b %d %Y %H:%M:%S", - ); - let mtime: String = fmt_time( - file.metadata().modified.unwrap_or(UNIX_EPOCH), - "%b %d %Y %H:%M:%S", - ); - texts - .add_row() - .add_col(TextSpan::from("Creation time: ")) - .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); - texts - .add_row() - .add_col(TextSpan::from("Last modified time: ")) - .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); - texts - .add_row() - .add_col(TextSpan::from("Last access time: ")) - .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); - // User - #[cfg(posix)] - let username: String = match file.metadata().uid { - Some(uid) => match get_user_by_uid(uid) { - Some(user) => user.name().to_string_lossy().to_string(), - None => uid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(win)] - let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); - // Group - #[cfg(posix)] - let group: String = match file.metadata().gid { - Some(gid) => match get_group_by_gid(gid) { - Some(group) => group.name().to_string_lossy().to_string(), - None => gid.to_string(), - }, - None => String::from("0"), - }; - #[cfg(win)] - let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); - texts - .add_row() - .add_col(TextSpan::from("User: ")) - .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); - texts - .add_row() - .add_col(TextSpan::from("Group: ")) - .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); - Self { - component: List::default() - .borders(Borders::default().modifiers(BorderType::Rounded)) - .scroll(false) - .title(file.name(), Alignment::Left) - .rows(texts.build()), - } - } -} - -impl Component for FileInfoPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Esc | Key::Enter, - .. - }) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)), - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct KeybindingsPopup { - component: List, -} - -impl KeybindingsPopup { - pub fn new(key_color: Color) -> Self { - Self { - component: List::default() - .borders(Borders::default().modifiers(BorderType::Rounded)) - .scroll(true) - .step(8) - .highlighted_str("? ") - .title("Keybindings", Alignment::Center) - .rewind(true) - .rows( - TableBuilder::default() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Disconnect")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to previous directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change explorer tab")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Move up/down in list")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Enter directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Upload/Download file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Switch between explorer and log window", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle hidden files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change file sorting mode")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Copy")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Make directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Search files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to path")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show help")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Show info about selected file", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Create symlink pointing to the current selected entry", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Reload directory content")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Create new file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open text file with preferred editor", - )) - .add_row() - .add_col(TextSpan::new("

").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle bottom panel")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Quit termscp")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Rename file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Save file as")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Watch/unwatch file changes")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to parent directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with default application for file type", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open file with specified application", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Execute shell command")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Toggle synchronized browsing", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change file permissions")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Filter files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Delete selected file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select all files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Deselect all files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Interrupt file transfer")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Get total path size of selected files", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show watched paths")) + .build(), + ), + } + } +} + +impl Component for KeybindingsPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)), + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/mkdir.rs b/src/ui/activities/filetransfer/components/popups/mkdir.rs new file mode 100644 index 0000000..7124181 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/mkdir.rs @@ -0,0 +1,164 @@ +use tui_realm_stdlib::{Input, Radio}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, PendingActionMsg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct MkdirPopup { + component: Input, +} + +impl MkdirPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New directory name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("directory-name", Alignment::Center), + } + } +} + +impl Component for MkdirPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseMkdirPopup)) + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct SyncBrowsingMkdirPopup { + component: Radio, +} + +impl SyncBrowsingMkdirPopup { + pub fn new(color: Color, dir_name: &str) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Yes", "No"]) + .title( + format!( + r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"# + ), + Alignment::Center, + ), + } + } +} + +impl Component for SyncBrowsingMkdirPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::PendingAction( + PendingActionMsg::CloseSyncBrowsingMkdirPopup, + )), + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::PendingAction( + PendingActionMsg::CloseSyncBrowsingMkdirPopup, + )), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)) + } else { + Some(Msg::PendingAction( + PendingActionMsg::CloseSyncBrowsingMkdirPopup, + )) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/newfile.rs b/src/ui/activities/filetransfer/components/popups/newfile.rs new file mode 100644 index 0000000..d32da4b --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/newfile.rs @@ -0,0 +1,91 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct NewfilePopup { + component: Input, +} + +impl NewfilePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "New file name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("file.txt", Alignment::Center), + } + } +} + +impl Component for NewfilePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseNewFilePopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/open_with.rs b/src/ui/activities/filetransfer/components/popups/open_with.rs new file mode 100644 index 0000000..e228dcc --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/open_with.rs @@ -0,0 +1,93 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct OpenWithPopup { + component: Input, +} + +impl OpenWithPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Open file with\u{2026}", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Type the program to open the file with", Alignment::Center), + } + } +} + +impl Component for OpenWithPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseOpenWithPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/progress_bar.rs b/src/ui/activities/filetransfer/components/popups/progress_bar.rs new file mode 100644 index 0000000..a6bc96b --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/progress_bar.rs @@ -0,0 +1,82 @@ +use tui_realm_stdlib::ProgressBar; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg}; + +#[derive(MockComponent)] +pub struct ProgressBarFull { + component: ProgressBar, +} + +impl ProgressBarFull { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarFull { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} + +#[derive(MockComponent)] +pub struct ProgressBarPartial { + component: ProgressBar, +} + +impl ProgressBarPartial { + pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { + Self { + component: ProgressBar::default() + .borders( + Borders::default() + .modifiers(BorderType::Rounded) + .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT), + ) + .foreground(color) + .label(label) + .progress(prog) + .title(title, Alignment::Center), + } + } +} + +impl Component for ProgressBarPartial { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortTransfer)) + } else { + None + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/quit.rs b/src/ui/activities/filetransfer/components/popups/quit.rs new file mode 100644 index 0000000..ad8fa95 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/quit.rs @@ -0,0 +1,71 @@ +use tui_realm_stdlib::Radio; +use tuirealm::command::{Cmd, CmdResult, Direction}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct QuitPopup { + component: Radio, +} + +impl QuitPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Yes", "No"]) + .title("Are you sure you want to quit termscp?", Alignment::Center), + } + } +} + +impl Component for QuitPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::Quit)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::CloseQuitPopup)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Quit)) + } else { + Some(Msg::Ui(UiMsg::CloseQuitPopup)) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/rename.rs b/src/ui/activities/filetransfer/components/popups/rename.rs new file mode 100644 index 0000000..f73f301 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/rename.rs @@ -0,0 +1,93 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct RenamePopup { + component: Input, +} + +impl RenamePopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Move file(s) to\u{2026}", Alignment::Center), + } + } +} + +impl Component for RenamePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::RenameFile(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseRenamePopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/replace.rs b/src/ui/activities/filetransfer/components/popups/replace.rs new file mode 100644 index 0000000..7114f26 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/replace.rs @@ -0,0 +1,83 @@ +use tui_realm_stdlib::Radio; +use tuirealm::command::{Cmd, CmdResult, Direction}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, PendingActionMsg}; + +#[derive(MockComponent)] +pub struct ReplacePopup { + component: Radio, +} + +impl ReplacePopup { + pub fn new(filename: Option<&str>, color: Color) -> Self { + let text = match filename { + Some(f) => format!(r#"File "{f}" already exists. Overwrite file?"#), + None => "Overwrite files?".to_string(), + }; + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"]) + .title(text, Alignment::Center), + } + } +} + +impl Component for ReplacePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.perform(Cmd::Submit) { + CmdResult::Submit(State::One(StateValue::Usize(0))) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)) + } + CmdResult::Submit(State::One(StateValue::Usize(1))) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)) + } + CmdResult::Submit(State::One(StateValue::Usize(2))) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)) + } + CmdResult::Submit(State::One(StateValue::Usize(3))) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll)) + } + CmdResult::Submit(State::One(StateValue::Usize(4))) => { + Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) + } + _ => Some(Msg::None), + }, + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/save_as.rs b/src/ui/activities/filetransfer/components/popups/save_as.rs new file mode 100644 index 0000000..63a84e2 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/save_as.rs @@ -0,0 +1,93 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct SaveAsPopup { + component: Input, +} + +impl SaveAsPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "/foo/bar/buzz.txt", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Save as\u{2026}", Alignment::Center), + } + } +} + +impl Component for SaveAsPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSaveAsPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/sorting.rs b/src/ui/activities/filetransfer/components/popups/sorting.rs new file mode 100644 index 0000000..7d60e26 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/sorting.rs @@ -0,0 +1,65 @@ +use tui_realm_stdlib::Radio; +use tuirealm::command::{Cmd, CmdResult, Direction}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::explorer::FileSorting; +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct SortingPopup { + component: Radio, +} + +impl SortingPopup { + pub fn new(value: FileSorting, color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Name", "Modify time", "Creation time", "Size"]) + .title("Sort files by\u{2026}", Alignment::Center) + .value(match value { + FileSorting::CreationTime => 2, + FileSorting::ModifyTime => 1, + FileSorting::Name => 0, + FileSorting::Size => 3, + FileSorting::None => 0, + }), + } + } +} + +impl Component for SortingPopup { + fn on(&mut self, ev: Event) -> Option { + let result = match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => self.perform(Cmd::Move(Direction::Left)), + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => self.perform(Cmd::Move(Direction::Right)), + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), + _ => return None, + }; + if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { + Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { + 0 => FileSorting::Name, + 1 => FileSorting::ModifyTime, + 2 => FileSorting::CreationTime, + 3 => FileSorting::Size, + _ => FileSorting::Name, + }))) + } else { + Some(Msg::None) + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/status_bar.rs b/src/ui/activities/filetransfer/components/popups/status_bar.rs new file mode 100644 index 0000000..f2b0a8c --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/status_bar.rs @@ -0,0 +1,87 @@ +use tui_realm_stdlib::Span; +use tuirealm::props::{Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +use crate::explorer::FileSorting; +use crate::ui::activities::filetransfer::Msg; +use crate::ui::activities::filetransfer::lib::browser::Browser; + +#[derive(MockComponent)] +pub struct StatusBarLocal { + component: Span, +} + +impl StatusBarLocal { + pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { + let file_sorting = file_sorting_label(browser.host_bridge().file_sorting); + let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible()); + Self { + component: Span::default().spans([ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarLocal { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct StatusBarRemote { + component: Span, +} + +impl StatusBarRemote { + pub fn new( + browser: &Browser, + sorting_color: Color, + hidden_color: Color, + sync_color: Color, + ) -> Self { + let file_sorting = file_sorting_label(browser.remote().file_sorting); + let hidden_files = hidden_files_label(browser.remote().hidden_files_visible()); + let sync_browsing = match browser.sync_browsing { + true => "ON ", + false => "OFF", + }; + Self { + component: Span::default().spans([ + TextSpan::new("File sorting: ").fg(sorting_color), + TextSpan::new(file_sorting).fg(sorting_color).reversed(), + TextSpan::new(" Hidden files: ").fg(hidden_color), + TextSpan::new(hidden_files).fg(hidden_color).reversed(), + TextSpan::new(" Sync browsing: ").fg(sync_color), + TextSpan::new(sync_browsing).fg(sync_color).reversed(), + ]), + } + } +} + +impl Component for StatusBarRemote { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +fn file_sorting_label(sorting: FileSorting) -> &'static str { + match sorting { + FileSorting::CreationTime => "By creation time", + FileSorting::ModifyTime => "By modify time", + FileSorting::Name => "By name", + FileSorting::Size => "By size", + FileSorting::None => "", + } +} + +fn hidden_files_label(visible: bool) -> &'static str { + match visible { + true => "Show", + false => "Hide", + } +} diff --git a/src/ui/activities/filetransfer/components/popups/symlink.rs b/src/ui/activities/filetransfer/components/popups/symlink.rs new file mode 100644 index 0000000..07b8f85 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/symlink.rs @@ -0,0 +1,96 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct SymlinkPopup { + component: Input, +} + +impl SymlinkPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "Symlink name", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title( + "Create a symlink pointing to the selected entry", + Alignment::Center, + ), + } + } +} + +impl Component for SymlinkPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::CreateSymlink(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseSymlinkPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/wait.rs b/src/ui/activities/filetransfer/components/popups/wait.rs new file mode 100644 index 0000000..cb72ad8 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/wait.rs @@ -0,0 +1,75 @@ +use tui_realm_stdlib::Paragraph; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg}; + +#[derive(MockComponent)] +pub struct WaitPopup { + component: Paragraph, +} + +impl WaitPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text([TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for WaitPopup { + fn on(&mut self, _ev: Event) -> Option { + None + } +} + +#[derive(MockComponent)] +pub struct WalkdirWaitPopup { + component: Paragraph, +} + +impl WalkdirWaitPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text([ + TextSpan::from(text.as_ref()), + TextSpan::from("Press 'CTRL+C' to abort"), + ]) + .wrap(true), + } + } +} + +impl Component for WalkdirWaitPopup { + fn on(&mut self, ev: Event) -> Option { + if matches!( + ev, + Event::Keyboard(KeyEvent { + code: Key::Char('c'), + modifiers: KeyModifiers::CONTROL + }) + ) { + Some(Msg::Transfer(TransferMsg::AbortWalkdir)) + } else { + None + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/watcher.rs b/src/ui/activities/filetransfer/components/popups/watcher.rs new file mode 100644 index 0000000..bf5e6f1 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/watcher.rs @@ -0,0 +1,162 @@ +use tui_realm_stdlib::{List, Radio}; +use tuirealm::command::{Cmd, CmdResult, Direction, Position}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct WatchedPathsList { + component: List, +} + +impl WatchedPathsList { + pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self { + Self { + component: List::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .rewind(true) + .scroll(true) + .step(4) + .highlighted_color(color) + .highlighted_str("\u{27a4} ") + .title( + "These files are currently synched with the remote host", + Alignment::Center, + ) + .rows( + paths + .iter() + .map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())]) + .collect(), + ), + } + } +} + +impl Component for WatchedPathsList { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseWatchedPathsList)) + } + Event::Keyboard(KeyEvent { + code: Key::Down, .. + }) => { + self.perform(Cmd::Move(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { + self.perform(Cmd::Move(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageDown, + .. + }) => { + self.perform(Cmd::Scroll(Direction::Down)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::PageUp, .. + }) => { + self.perform(Cmd::Scroll(Direction::Up)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + // get state + if let State::One(StateValue::Usize(idx)) = self.component.state() { + Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx))) + } else { + Some(Msg::None) + } + } + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct WatcherPopup { + component: Radio, +} + +impl WatcherPopup { + pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self { + let text = match watched { + false => format!(r#"Synchronize changes from "{local}" to "{remote}"?"#), + true => format!(r#"Stop synchronizing changes at "{local}"?"#), + }; + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Yes", "No"]) + .title(text, Alignment::Center), + } + } +} + +impl Component for WatcherPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseWatcherPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::ToggleWatch)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::CloseWatcherPopup)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::ToggleWatch)) + } else { + Some(Msg::Ui(UiMsg::CloseWatcherPopup)) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/terminal/component.rs b/src/ui/activities/filetransfer/components/terminal/component.rs index 3d66533..1339653 100644 --- a/src/ui/activities/filetransfer/components/terminal/component.rs +++ b/src/ui/activities/filetransfer/components/terminal/component.rs @@ -84,6 +84,54 @@ impl TerminalComponent { } impl MockComponent for TerminalComponent { + fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) { + let width = area.width.saturating_sub(2); + let height = area.height.saturating_sub(2); + + // update the terminal size if it has changed + if self.size != (width, height) { + self.size = (width, height); + self.parser.set_size(height, width); + } + + let title = self + .query(Attribute::Title) + .map(|value| value.unwrap_string()) + .unwrap_or_else(|| "Terminal".to_string()); + + let fg = self + .query(Attribute::Foreground) + .map(|value| value.unwrap_color()) + .unwrap_or(tuirealm::ratatui::style::Color::Reset); + + let bg = self + .query(Attribute::Background) + .map(|value| value.unwrap_color()) + .unwrap_or(tuirealm::ratatui::style::Color::Reset); + + let border_color = self + .query(Attribute::Borders) + .map(|value| value.unwrap_color()) + .unwrap_or(tuirealm::ratatui::style::Color::Reset); + + let terminal = PseudoTerminal::new(self.parser.screen()) + .block( + Block::default() + .title(title) + .border_type(BorderType::Rounded) + .border_style(Style::default().fg(border_color)) + .borders(BorderSides::ALL) + .style(Style::default().fg(fg).bg(bg)), + ) + .style(Style::default().fg(fg).bg(bg)); + + frame.render_widget(terminal, area); + } + + fn query(&self, attr: tuirealm::Attribute) -> Option { + self.props.get(attr) + } + fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) { if attr == Attribute::Text { if let tuirealm::AttrValue::String(s) = value { @@ -97,6 +145,10 @@ impl MockComponent for TerminalComponent { } } + fn state(&self) -> State { + State::One(StateValue::String(self.line.content().to_string())) + } + fn perform(&mut self, cmd: Cmd) -> CmdResult { match cmd { Cmd::Type(s) => { @@ -234,56 +286,4 @@ impl MockComponent for TerminalComponent { _ => CmdResult::None, } } - - fn query(&self, attr: tuirealm::Attribute) -> Option { - self.props.get(attr) - } - - fn state(&self) -> State { - State::One(StateValue::String(self.line.content().to_string())) - } - - fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) { - let width = area.width.saturating_sub(2); - let height = area.height.saturating_sub(2); - - // update the terminal size if it has changed - if self.size != (width, height) { - self.size = (width, height); - self.parser.set_size(height, width); - } - - let title = self - .query(Attribute::Title) - .map(|value| value.unwrap_string()) - .unwrap_or_else(|| "Terminal".to_string()); - - let fg = self - .query(Attribute::Foreground) - .map(|value| value.unwrap_color()) - .unwrap_or(tuirealm::ratatui::style::Color::Reset); - - let bg = self - .query(Attribute::Background) - .map(|value| value.unwrap_color()) - .unwrap_or(tuirealm::ratatui::style::Color::Reset); - - let border_color = self - .query(Attribute::Borders) - .map(|value| value.unwrap_color()) - .unwrap_or(tuirealm::ratatui::style::Color::Reset); - - let terminal = PseudoTerminal::new(self.parser.screen()) - .block( - Block::default() - .title(title) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(border_color)) - .borders(BorderSides::ALL) - .style(Style::default().fg(fg).bg(bg)), - ) - .style(Style::default().fg(fg).bg(bg)); - - frame.render_widget(terminal, area); - } } diff --git a/src/ui/activities/filetransfer/fswatcher.rs b/src/ui/activities/filetransfer/fswatcher.rs index ae09bf9..5f10fee 100644 --- a/src/ui/activities/filetransfer/fswatcher.rs +++ b/src/ui/activities/filetransfer/fswatcher.rs @@ -52,7 +52,7 @@ impl FileTransferActivity { destination.display() ); // stat fs entry - let origin = match self.client.stat(source) { + let origin = match self.browser.remote_pane_mut().fs.stat(source) { Ok(f) => f, Err(err) => { self.log( @@ -71,7 +71,18 @@ impl FileTransferActivity { } fn remove_watched_file(&mut self, file: &Path) { - match self.client.remove_dir_all(file) { + // stat the file first to use HostBridge::remove + let entry = match self.browser.remote_pane_mut().fs.stat(file) { + Ok(e) => e, + Err(err) => { + self.log( + LogLevel::Error, + format!("failed to stat watched file {}: {}", file.display(), err), + ); + return; + } + }; + match self.browser.remote_pane_mut().fs.remove(&entry) { Ok(()) => { self.log( LogLevel::Info, @@ -89,7 +100,7 @@ impl FileTransferActivity { fn upload_watched_file(&mut self, host: &Path, remote: &Path) { // stat host file - let entry = match self.host_bridge.stat(host) { + let entry = match self.browser.local_pane_mut().fs.stat(host) { Ok(e) => e, Err(err) => { self.log( diff --git a/src/ui/activities/filetransfer/lib/browser.rs b/src/ui/activities/filetransfer/lib/browser.rs index 52d7241..052d03b 100644 --- a/src/ui/activities/filetransfer/lib/browser.rs +++ b/src/ui/activities/filetransfer/lib/browser.rs @@ -1,12 +1,13 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use std::path::Path; use nucleo::Utf32String; use remotefs::File; +use super::pane::Pane; use crate::explorer::builder::FileExplorerBuilder; use crate::explorer::{FileExplorer, FileSorting}; use crate::system::config_client::ConfigClient; @@ -31,19 +32,19 @@ pub enum FoundExplorerTab { /// Browser contains the browser options pub struct Browser { - host_bridge: FileExplorer, // Local File explorer state - remote: FileExplorer, // Remote File explorer state - found: Option, // File explorer for find result - tab: FileExplorerTab, // Current selected tab + local: Pane, + remote: Pane, + found: Option, // File explorer for find result + tab: FileExplorerTab, // Current selected tab pub sync_browsing: bool, } impl Browser { /// Build a new `Browser` struct - pub fn new(cli: &ConfigClient) -> Self { + pub fn new(local: Pane, remote: Pane) -> Self { Self { - host_bridge: Self::build_local_explorer(cli), - remote: Self::build_remote_explorer(cli), + local, + remote, found: None, tab: FileExplorerTab::HostBridge, sync_browsing: false, @@ -52,8 +53,8 @@ impl Browser { pub fn explorer(&self) -> &FileExplorer { match self.tab { - FileExplorerTab::HostBridge => &self.host_bridge, - FileExplorerTab::Remote => &self.remote, + FileExplorerTab::HostBridge => &self.local.explorer, + FileExplorerTab::Remote => &self.remote.explorer, FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.found.as_ref().map(|x| &x.explorer).unwrap() } @@ -62,15 +63,15 @@ impl Browser { pub fn other_explorer_no_found(&self) -> &FileExplorer { match self.tab { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote, - FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.host_bridge, + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote.explorer, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.local.explorer, } } pub fn explorer_mut(&mut self) -> &mut FileExplorer { match self.tab { - FileExplorerTab::HostBridge => &mut self.host_bridge, - FileExplorerTab::Remote => &mut self.remote, + FileExplorerTab::HostBridge => &mut self.local.explorer, + FileExplorerTab::Remote => &mut self.remote.explorer, FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.found.as_mut().map(|x| &mut x.explorer).unwrap() } @@ -78,19 +79,19 @@ impl Browser { } pub fn host_bridge(&self) -> &FileExplorer { - &self.host_bridge + &self.local.explorer } pub fn host_bridge_mut(&mut self) -> &mut FileExplorer { - &mut self.host_bridge + &mut self.local.explorer } pub fn remote(&self) -> &FileExplorer { - &self.remote + &self.remote.explorer } pub fn remote_mut(&mut self) -> &mut FileExplorer { - &mut self.remote + &mut self.remote.explorer } pub fn found(&self) -> Option<&FileExplorer> { @@ -151,22 +152,70 @@ impl Browser { /// Toggle terminal for the current tab pub fn toggle_terminal(&mut self, terminal: bool) { if self.tab == FileExplorerTab::HostBridge { - self.host_bridge.toggle_terminal(terminal); + self.local.explorer.toggle_terminal(terminal); } else if self.tab == FileExplorerTab::Remote { - self.remote.toggle_terminal(terminal); + self.remote.explorer.toggle_terminal(terminal); } } /// Check if terminal is open for the host bridge tab pub fn is_terminal_open_host_bridge(&self) -> bool { - self.tab == FileExplorerTab::HostBridge && self.host_bridge.terminal_open() + self.tab == FileExplorerTab::HostBridge && self.local.explorer.terminal_open() } /// Check if terminal is open for the remote tab pub fn is_terminal_open_remote(&self) -> bool { - self.tab == FileExplorerTab::Remote && self.remote.terminal_open() + self.tab == FileExplorerTab::Remote && self.remote.explorer.terminal_open() } + // -- Pane accessors -- + + /// The pane whose filesystem is targeted by the current tab. + pub fn fs_pane(&self) -> &Pane { + match self.tab { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.local, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.remote, + } + } + + /// The pane whose filesystem is targeted by the current tab (mutable). + pub fn fs_pane_mut(&mut self) -> &mut Pane { + match self.tab { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &mut self.local, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => &mut self.remote, + } + } + + /// The opposite pane (transfer destination). + pub fn opposite_pane(&self) -> &Pane { + match self.tab { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => &self.remote, + FileExplorerTab::Remote | FileExplorerTab::FindRemote => &self.local, + } + } + + /// Direct access to local pane + pub fn local_pane(&self) -> &Pane { + &self.local + } + + /// Direct access to local pane (mutable) + pub fn local_pane_mut(&mut self) -> &mut Pane { + &mut self.local + } + + /// Direct access to remote pane + pub fn remote_pane(&self) -> &Pane { + &self.remote + } + + /// Direct access to remote pane (mutable) + pub fn remote_pane_mut(&mut self) -> &mut Pane { + &mut self.remote + } + + // -- Explorer builders (static helpers) -- + /// Build a file explorer with local host setup pub fn build_local_explorer(cli: &ConfigClient) -> FileExplorer { let mut builder = Self::build_explorer(cli); diff --git a/src/ui/activities/filetransfer/lib/mod.rs b/src/ui/activities/filetransfer/lib/mod.rs index 6d38c4c..b076717 100644 --- a/src/ui/activities/filetransfer/lib/mod.rs +++ b/src/ui/activities/filetransfer/lib/mod.rs @@ -1,7 +1,8 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall pub(crate) mod browser; +pub(crate) mod pane; pub(crate) mod transfer; pub(crate) mod walkdir; diff --git a/src/ui/activities/filetransfer/lib/pane.rs b/src/ui/activities/filetransfer/lib/pane.rs new file mode 100644 index 0000000..1a65551 --- /dev/null +++ b/src/ui/activities/filetransfer/lib/pane.rs @@ -0,0 +1,57 @@ +use crate::explorer::FileExplorer; +use crate::host::HostBridge; + +/// One side of the dual-pane file browser. +/// +/// Holds the file explorer state, connection tracking, and the filesystem +/// client (`HostBridge`) used to interact with the underlying storage +/// (local or remote). +pub struct Pane { + /// File explorer state (directory listing, sorting, filtering, transfer queue) + pub(crate) explorer: FileExplorer, + /// Whether this pane has been connected at least once + pub(crate) connected: bool, + /// Filesystem client for this pane + pub(crate) fs: Box, +} + +impl Pane { + /// Create a new Pane. + pub fn new(explorer: FileExplorer, connected: bool, fs: Box) -> Self { + Self { + explorer, + connected, + fs, + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + use crate::explorer::builder::FileExplorerBuilder; + use crate::host::Localhost; + + fn make_pane() -> Pane { + let wrkdir = std::env::temp_dir(); + let explorer = FileExplorerBuilder::new().build(); + let fs = Localhost::new(wrkdir).unwrap(); + Pane::new(explorer, false, Box::new(fs)) + } + + #[test] + fn test_pane_new() { + let pane = make_pane(); + assert!(!pane.connected); + assert!(pane.fs.is_localhost()); + } + + #[test] + fn test_pane_pwd() { + let mut pane = make_pane(); + let pwd = pane.fs.pwd().unwrap(); + assert_eq!(pwd, PathBuf::from(std::env::temp_dir())); + } +} diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs index 165e18e..c14283a 100644 --- a/src/ui/activities/filetransfer/lib/transfer.rs +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall use std::fmt; use std::time::Instant; diff --git a/src/ui/activities/filetransfer/misc.rs b/src/ui/activities/filetransfer/misc.rs index 6c0362c..435a210 100644 --- a/src/ui/activities/filetransfer/misc.rs +++ b/src/ui/activities/filetransfer/misc.rs @@ -1,22 +1,11 @@ -use std::env; -use std::path::{Path, PathBuf}; +mod filelist; +mod host; +mod log; +mod notify; -use bytesize::ByteSize; -use tuirealm::props::{ - Alignment, AttrValue, Attribute, Color, PropPayload, PropValue, TableBuilder, TextModifiers, - TextSpan, -}; use tuirealm::{PollStrategy, Update}; -use super::browser::FileExplorerTab; -use super::{ConfigClient, FileTransferActivity, Id, LogLevel, LogRecord, TransferPayload}; -use crate::filetransfer::{HostBridgeParams, ProtocolParams}; -use crate::system::environment; -use crate::system::notifications::Notification; -use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex}; -use crate::utils::path; - -const LOG_CAPACITY: usize = 256; +use super::FileTransferActivity; impl FileTransferActivity { /// Call `Application::tick()` and process messages in `Update` @@ -38,563 +27,4 @@ impl FileTransferActivity { } } } - - /// Add message to log events - pub(super) fn log(&mut self, level: LogLevel, msg: String) { - // Log to file - match level { - LogLevel::Error => error!("{}", msg), - LogLevel::Info => info!("{}", msg), - LogLevel::Warn => warn!("{}", msg), - } - // Create log record - let record: LogRecord = LogRecord::new(level, msg); - //Check if history overflows the size - if self.log_records.len() + 1 > LOG_CAPACITY { - self.log_records.pop_back(); // Start cleaning events from back - } - // Eventually push front the new record - self.log_records.push_front(record); - // Update log - self.update_logbox(); - // flag redraw - self.redraw = true; - } - - /// Add message to log events and also display it as an alert - pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) { - self.mount_error(msg.as_str()); - self.log(level, msg); - // Update log - self.update_logbox(); - } - - /// Initialize configuration client if possible. - /// This function doesn't return errors. - pub(super) fn init_config_client() -> ConfigClient { - match environment::init_config_dir() { - Ok(termscp_dir) => match termscp_dir { - Some(termscp_dir) => { - // Make configuration file path and ssh keys path - let (config_path, ssh_keys_path): (PathBuf, PathBuf) = - environment::get_config_paths(termscp_dir.as_path()); - match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) { - Ok(config_client) => config_client, - Err(_) => ConfigClient::degraded(), - } - } - None => ConfigClient::degraded(), - }, - Err(_) => ConfigClient::degraded(), - } - } - - /// Set text editor to use - pub(super) fn setup_text_editor(&self) { - unsafe { - env::set_var("EDITOR", self.config().get_text_editor()); - } - } - - /// Convert a path to absolute according to host explorer - pub(super) fn host_bridge_to_abs_path(&self, path: &Path) -> PathBuf { - path::absolutize(self.host_bridge().wrkdir.as_path(), path) - } - - /// Convert a path to absolute according to remote explorer - pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf { - path::absolutize(self.remote().wrkdir.as_path(), path) - } - - /// Get remote hostname - pub(super) fn get_remote_hostname(&self) -> String { - let ft_params = self.context().remote_params().unwrap(); - self.get_hostname(&ft_params.params) - } - - pub(super) fn get_hostbridge_hostname(&self) -> String { - let host_bridge_params = self.context().host_bridge_params().unwrap(); - match host_bridge_params { - HostBridgeParams::Localhost(_) => { - let hostname = match hostname::get() { - Ok(h) => h, - Err(_) => return String::from("localhost"), - }; - let hostname: String = hostname.as_os_str().to_string_lossy().to_string(); - let tokens: Vec<&str> = hostname.split('.').collect(); - String::from(*tokens.first().unwrap_or(&"localhost")) - } - HostBridgeParams::Remote(_, params) => self.get_hostname(params), - } - } - - fn get_hostname(&self, params: &ProtocolParams) -> String { - match params { - ProtocolParams::Generic(params) => params.address.clone(), - ProtocolParams::AwsS3(params) => params.bucket_name.clone(), - ProtocolParams::Kube(params) => { - params.namespace.clone().unwrap_or("default".to_string()) - } - ProtocolParams::Smb(params) => params.address.clone(), - ProtocolParams::WebDAV(params) => params.uri.clone(), - } - } - - /// Get connection message to show to client - pub(super) fn get_connection_msg(params: &ProtocolParams) -> String { - match params { - ProtocolParams::Generic(params) => { - info!( - "Client is not connected to remote; connecting to {}:{}", - params.address, params.port - ); - format!("Connecting to {}:{}…", params.address, params.port) - } - ProtocolParams::AwsS3(params) => { - info!( - "Client is not connected to remote; connecting to {}{} ({})", - params.endpoint.as_deref().unwrap_or(""), - params.bucket_name, - params.region.as_deref().unwrap_or("custom") - ); - format!("Connecting to {}…", params.bucket_name) - } - ProtocolParams::Kube(params) => { - let namespace = params.namespace.as_deref().unwrap_or("default"); - info!("Client is not connected to remote; connecting to namespace {namespace}",); - format!("Connecting to Kube namespace {namespace}…",) - } - ProtocolParams::Smb(params) => { - info!( - "Client is not connected to remote; connecting to {}:{}", - params.address, params.share - ); - format!("Connecting to \\\\{}\\{}…", params.address, params.share) - } - ProtocolParams::WebDAV(params) => { - info!( - "Client is not connected to remote; connecting to {}", - params.uri - ); - format!("Connecting to {}…", params.uri) - } - } - } - - /// Send notification regarding transfer completed - /// The notification is sent only when these conditions are satisfied: - /// - /// - notifications are enabled - /// - transfer size is greater or equal than notification threshold - pub(super) fn notify_transfer_completed(&self, payload: &TransferPayload) { - if self.config().get_notifications() - && self.config().get_notification_threshold() as usize <= self.transfer.full_size() - { - Notification::transfer_completed(self.transfer_completed_msg(payload)); - } - } - - /// Send notification regarding transfer error - /// The notification is sent only when these conditions are satisfied: - /// - /// - notifications are enabled - /// - transfer size is greater or equal than notification threshold - pub(super) fn notify_transfer_error(&self, msg: &str) { - if self.config().get_notifications() - && self.config().get_notification_threshold() as usize <= self.transfer.full_size() - { - Notification::transfer_error(msg); - } - } - - fn transfer_completed_msg(&self, payload: &TransferPayload) -> String { - let transfer_stats = format!( - "took {} seconds; at {}/s", - fmt_millis(self.transfer.partial.started().elapsed()), - ByteSize(self.transfer.partial.calc_bytes_per_second()), - ); - match payload { - TransferPayload::File(file) => { - format!( - "File \"{}\" has been successfully transferred ({})", - file.name(), - transfer_stats - ) - } - TransferPayload::Any(entry) => { - format!( - "\"{}\" has been successfully transferred ({})", - entry.name(), - transfer_stats - ) - } - TransferPayload::TransferQueue(entries) => { - format!( - "{} files has been successfully transferred ({})", - entries.len(), - transfer_stats - ) - } - } - } - - /// Update host bridge file list - pub(super) fn update_host_bridge_filelist(&mut self) { - self.reload_host_bridge_dir(); - self.reload_host_bridge_filelist(); - } - - /// Update host bridge file list - pub(super) fn reload_host_bridge_filelist(&mut self) { - // Get width - let width = self - .context_mut() - .terminal() - .raw() - .size() - .map(|x| (x.width / 2) - 2) - .unwrap_or(0) as usize; - let hostname = self.get_hostbridge_hostname(); - - let hostname: String = format!( - "{hostname}:{} ", - fmt_path_elide_ex( - self.host_bridge().wrkdir.as_path(), - width, - hostname.len() + 3 - ) // 3 because of '/…/' - ); - let files: Vec> = self - .host_bridge() - .iter_files() - .map(|x| { - let mut span = TextSpan::from(self.host_bridge().fmt_file(x)); - if self.host_bridge().enqueued().contains_key(x.path()) { - span.modifiers |= - TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; - } - - vec![span] - }) - .collect(); - // Update content and title - assert!( - self.app - .attr( - &Id::ExplorerHostBridge, - Attribute::Content, - AttrValue::Table(files) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ExplorerHostBridge, - Attribute::Title, - AttrValue::Title((hostname, Alignment::Left)) - ) - .is_ok() - ); - } - - /// Update remote file list - pub(super) fn update_remote_filelist(&mut self) { - self.reload_remote_dir(); - self.reload_remote_filelist(); - } - - pub(super) fn get_tab_hostname(&self) -> String { - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - self.get_hostbridge_hostname() - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(), - } - } - - pub(super) fn terminal_prompt(&self) -> String { - const TERM_CYAN: &str = "\x1b[36m"; - const TERM_GREEN: &str = "\x1b[32m"; - const TERM_YELLOW: &str = "\x1b[33m"; - const TERM_RESET: &str = "\x1b[0m"; - - let panel = self.browser.tab(); - match panel { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - let username = self - .context() - .host_bridge_params() - .and_then(|params| { - params - .username() - .map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@")) - }) - .unwrap_or("".to_string()); - let hostname = self.get_hostbridge_hostname(); - format!( - "{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ", - fmt_path_elide_ex( - self.host_bridge().wrkdir.as_path(), - 0, - hostname.len() + 3 // 3 because of '/…/' - ) - ) - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => { - let username = self - .context() - .remote_params() - .and_then(|params| { - params - .username() - .map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@")) - }) - .unwrap_or("".to_string()); - let hostname = self.get_remote_hostname(); - let fmt_path = fmt_path_elide_ex( - self.remote().wrkdir.as_path(), - 0, - hostname.len() + 3, // 3 because of '/…/' - ); - let fmt_path = if fmt_path.starts_with('/') { - fmt_path - } else { - format!("/{}", fmt_path) - }; - - format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",) - } - } - } - - pub(super) fn reload_remote_filelist(&mut self) { - let width = self - .context_mut() - .terminal() - .raw() - .size() - .map(|x| (x.width / 2) - 2) - .unwrap_or(0) as usize; - let hostname = self.get_remote_hostname(); - let hostname: String = format!( - "{}:{} ", - hostname, - fmt_path_elide_ex( - self.remote().wrkdir.as_path(), - width, - hostname.len() + 3 // 3 because of '/…/' - ) - ); - let files: Vec> = self - .remote() - .iter_files() - .map(|x| { - let mut span = TextSpan::from(self.remote().fmt_file(x)); - if self.remote().enqueued().contains_key(x.path()) { - span.modifiers |= - TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; - } - - vec![span] - }) - .collect(); - // Update content and title - assert!( - self.app - .attr( - &Id::ExplorerRemote, - Attribute::Content, - AttrValue::Table(files) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ExplorerRemote, - Attribute::Title, - AttrValue::Title((hostname, Alignment::Left)) - ) - .is_ok() - ); - } - - /// Update log box - pub(super) fn update_logbox(&mut self) { - let mut table: TableBuilder = TableBuilder::default(); - for (idx, record) in self.log_records.iter().enumerate() { - // Add row if not first row - if idx > 0 { - table.add_row(); - } - let fg = match record.level { - LogLevel::Error => Color::Red, - LogLevel::Warn => Color::Yellow, - LogLevel::Info => Color::Green, - }; - table - .add_col(TextSpan::from(format!( - "{}", - record.time.format("%Y-%m-%dT%H:%M:%S%Z") - ))) - .add_col(TextSpan::from(" [")) - .add_col( - TextSpan::new( - format!( - "{:5}", - match record.level { - LogLevel::Error => "ERROR", - LogLevel::Warn => "WARN", - LogLevel::Info => "INFO", - } - ) - .as_str(), - ) - .fg(fg), - ) - .add_col(TextSpan::from("]: ")) - .add_col(TextSpan::from(record.msg.as_str())); - } - assert!( - self.app - .attr( - &Id::Log, - Attribute::Content, - AttrValue::Table(table.build()) - ) - .is_ok() - ); - } - - pub(super) fn update_progress_bar(&mut self, filename: String) { - assert!( - self.app - .attr( - &Id::ProgressBarFull, - Attribute::Text, - AttrValue::String(self.transfer.full.to_string()) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ProgressBarFull, - Attribute::Value, - AttrValue::Payload(PropPayload::One(PropValue::F64( - self.transfer.full.calc_progress() - ))) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ProgressBarPartial, - Attribute::Text, - AttrValue::String(self.transfer.partial.to_string()) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ProgressBarPartial, - Attribute::Value, - AttrValue::Payload(PropPayload::One(PropValue::F64( - self.transfer.partial.calc_progress() - ))) - ) - .is_ok() - ); - assert!( - self.app - .attr( - &Id::ProgressBarPartial, - Attribute::Title, - AttrValue::Title((filename, Alignment::Center)) - ) - .is_ok() - ); - } - - /// Finalize find process - pub(super) fn finalize_find(&mut self) { - // Set found to none - self.browser.del_found(); - // Restore tab - let new_tab = match self.browser.tab() { - FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge, - FileExplorerTab::FindRemote => FileExplorerTab::Remote, - _ => FileExplorerTab::HostBridge, - }; - // Give focus to new tab - match new_tab { - FileExplorerTab::HostBridge => { - assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()) - } - FileExplorerTab::Remote => { - assert!(self.app.active(&Id::ExplorerRemote).is_ok()) - } - FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { - assert!(self.app.active(&Id::ExplorerFind).is_ok()) - } - } - self.browser.change_tab(new_tab); - } - - pub(super) fn update_find_list(&mut self) { - let files: Vec> = self - .found() - .unwrap() - .iter_files() - .map(|x| { - let mut span = TextSpan::from(self.found().unwrap().fmt_file(x)); - if self.found().unwrap().enqueued().contains_key(x.path()) { - span.modifiers |= - TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; - } - vec![span] - }) - .collect(); - assert!( - self.app - .attr( - &Id::ExplorerFind, - Attribute::Content, - AttrValue::Table(files) - ) - .is_ok() - ); - } - - pub(super) fn update_browser_file_list(&mut self) { - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - self.update_host_bridge_filelist() - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(), - } - } - - pub(super) fn reload_browser_file_list(&mut self) { - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - self.reload_host_bridge_filelist() - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.reload_remote_filelist(), - } - } - - pub(super) fn update_browser_file_list_swapped(&mut self) { - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - self.update_remote_filelist() - } - FileExplorerTab::Remote | FileExplorerTab::FindRemote => { - self.update_host_bridge_filelist() - } - } - } } diff --git a/src/ui/activities/filetransfer/misc/filelist.rs b/src/ui/activities/filetransfer/misc/filelist.rs new file mode 100644 index 0000000..37579a5 --- /dev/null +++ b/src/ui/activities/filetransfer/misc/filelist.rs @@ -0,0 +1,285 @@ +use tuirealm::props::{ + Alignment, AttrValue, Attribute, PropPayload, PropValue, TextModifiers, TextSpan, +}; + +use super::super::browser::FileExplorerTab; +use super::super::{FileTransferActivity, Id, ui_result}; +use crate::utils::fmt::fmt_path_elide_ex; + +impl FileTransferActivity { + /// Update host bridge file list + pub(in crate::ui::activities::filetransfer) fn update_host_bridge_filelist(&mut self) { + self.reload_host_bridge_dir(); + self.reload_host_bridge_filelist(); + } + + /// Update host bridge file list + pub(in crate::ui::activities::filetransfer) fn reload_host_bridge_filelist(&mut self) { + // Get width + let width = self + .context_mut() + .terminal() + .raw() + .size() + .map(|x| (x.width / 2) - 2) + .unwrap_or(0) as usize; + let hostname = self.get_hostbridge_hostname(); + + let hostname: String = format!( + "{hostname}:{} ", + fmt_path_elide_ex( + self.host_bridge().wrkdir.as_path(), + width, + hostname.len() + 3 + ) // 3 because of '/…/' + ); + let files: Vec> = self + .host_bridge() + .iter_files() + .map(|x| { + let mut span = TextSpan::from(self.host_bridge().fmt_file(x)); + if self.host_bridge().enqueued().contains_key(x.path()) { + span.modifiers |= + TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; + } + + vec![span] + }) + .collect(); + // Update content and title + ui_result(self.app.attr( + &Id::ExplorerHostBridge, + Attribute::Content, + AttrValue::Table(files), + )); + ui_result(self.app.attr( + &Id::ExplorerHostBridge, + Attribute::Title, + AttrValue::Title((hostname, Alignment::Left)), + )); + } + + /// Update remote file list + pub(in crate::ui::activities::filetransfer) fn update_remote_filelist(&mut self) { + self.reload_remote_dir(); + self.reload_remote_filelist(); + } + + pub(in crate::ui::activities::filetransfer) fn get_tab_hostname(&self) -> String { + match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.get_hostbridge_hostname() + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.get_remote_hostname(), + } + } + + pub(in crate::ui::activities::filetransfer) fn terminal_prompt(&self) -> String { + const TERM_CYAN: &str = "\x1b[36m"; + const TERM_GREEN: &str = "\x1b[32m"; + const TERM_YELLOW: &str = "\x1b[33m"; + const TERM_RESET: &str = "\x1b[0m"; + + let panel = self.browser.tab(); + match panel { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + let username = self + .context() + .host_bridge_params() + .and_then(|params| { + params + .username() + .map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@")) + }) + .unwrap_or("".to_string()); + let hostname = self.get_hostbridge_hostname(); + format!( + "{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{}{TERM_RESET}$ ", + fmt_path_elide_ex( + self.host_bridge().wrkdir.as_path(), + 0, + hostname.len() + 3 // 3 because of '/…/' + ) + ) + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => { + let username = self + .context() + .remote_params() + .and_then(|params| { + params + .username() + .map(|u| format!("{TERM_CYAN}{u}{TERM_RESET}@")) + }) + .unwrap_or("".to_string()); + let hostname = self.get_remote_hostname(); + let fmt_path = fmt_path_elide_ex( + self.remote().wrkdir.as_path(), + 0, + hostname.len() + 3, // 3 because of '/…/' + ); + let fmt_path = if fmt_path.starts_with('/') { + fmt_path + } else { + format!("/{}", fmt_path) + }; + + format!("{username}{TERM_GREEN}{hostname}:{TERM_YELLOW}{fmt_path}{TERM_RESET}$ ",) + } + } + } + + pub(in crate::ui::activities::filetransfer) fn reload_remote_filelist(&mut self) { + let width = self + .context_mut() + .terminal() + .raw() + .size() + .map(|x| (x.width / 2) - 2) + .unwrap_or(0) as usize; + let hostname = self.get_remote_hostname(); + let hostname: String = format!( + "{}:{} ", + hostname, + fmt_path_elide_ex( + self.remote().wrkdir.as_path(), + width, + hostname.len() + 3 // 3 because of '/…/' + ) + ); + let files: Vec> = self + .remote() + .iter_files() + .map(|x| { + let mut span = TextSpan::from(self.remote().fmt_file(x)); + if self.remote().enqueued().contains_key(x.path()) { + span.modifiers |= + TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; + } + + vec![span] + }) + .collect(); + // Update content and title + ui_result(self.app.attr( + &Id::ExplorerRemote, + Attribute::Content, + AttrValue::Table(files), + )); + ui_result(self.app.attr( + &Id::ExplorerRemote, + Attribute::Title, + AttrValue::Title((hostname, Alignment::Left)), + )); + } + + pub(in crate::ui::activities::filetransfer) fn update_progress_bar( + &mut self, + filename: String, + ) { + ui_result(self.app.attr( + &Id::ProgressBarFull, + Attribute::Text, + AttrValue::String(self.transfer.full.to_string()), + )); + ui_result(self.app.attr( + &Id::ProgressBarFull, + Attribute::Value, + AttrValue::Payload(PropPayload::One(PropValue::F64( + self.transfer.full.calc_progress(), + ))), + )); + ui_result(self.app.attr( + &Id::ProgressBarPartial, + Attribute::Text, + AttrValue::String(self.transfer.partial.to_string()), + )); + ui_result(self.app.attr( + &Id::ProgressBarPartial, + Attribute::Value, + AttrValue::Payload(PropPayload::One(PropValue::F64( + self.transfer.partial.calc_progress(), + ))), + )); + ui_result(self.app.attr( + &Id::ProgressBarPartial, + Attribute::Title, + AttrValue::Title((filename, Alignment::Center)), + )); + } + + /// Finalize find process + pub(in crate::ui::activities::filetransfer) fn finalize_find(&mut self) { + // Set found to none + self.browser.del_found(); + // Restore tab + let new_tab = match self.browser.tab() { + FileExplorerTab::FindHostBridge => FileExplorerTab::HostBridge, + FileExplorerTab::FindRemote => FileExplorerTab::Remote, + _ => FileExplorerTab::HostBridge, + }; + // Give focus to new tab + match new_tab { + FileExplorerTab::HostBridge => { + ui_result(self.app.active(&Id::ExplorerHostBridge)); + } + FileExplorerTab::Remote => { + ui_result(self.app.active(&Id::ExplorerRemote)); + } + FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { + ui_result(self.app.active(&Id::ExplorerFind)); + } + } + self.browser.change_tab(new_tab); + } + + pub(in crate::ui::activities::filetransfer) fn update_find_list(&mut self) { + let files: Vec> = self + .found() + .unwrap() + .iter_files() + .map(|x| { + let mut span = TextSpan::from(self.found().unwrap().fmt_file(x)); + if self.found().unwrap().enqueued().contains_key(x.path()) { + span.modifiers |= + TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; + } + vec![span] + }) + .collect(); + ui_result(self.app.attr( + &Id::ExplorerFind, + Attribute::Content, + AttrValue::Table(files), + )); + } + + pub(in crate::ui::activities::filetransfer) fn update_browser_file_list(&mut self) { + match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.update_host_bridge_filelist() + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.update_remote_filelist(), + } + } + + pub(in crate::ui::activities::filetransfer) fn reload_browser_file_list(&mut self) { + match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.reload_host_bridge_filelist() + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => self.reload_remote_filelist(), + } + } + + pub(in crate::ui::activities::filetransfer) fn update_browser_file_list_swapped(&mut self) { + match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { + self.update_remote_filelist() + } + FileExplorerTab::Remote | FileExplorerTab::FindRemote => { + self.update_host_bridge_filelist() + } + } + } +} diff --git a/src/ui/activities/filetransfer/misc/host.rs b/src/ui/activities/filetransfer/misc/host.rs new file mode 100644 index 0000000..c40c87d --- /dev/null +++ b/src/ui/activities/filetransfer/misc/host.rs @@ -0,0 +1,118 @@ +use std::env; +use std::path::{Path, PathBuf}; + +use super::super::{ConfigClient, FileTransferActivity}; +use crate::filetransfer::{HostBridgeParams, ProtocolParams}; +use crate::system::environment; +use crate::utils::path; + +impl FileTransferActivity { + /// Initialize configuration client if possible. + /// This function doesn't return errors. + pub(in crate::ui::activities::filetransfer) fn init_config_client() -> ConfigClient { + match environment::init_config_dir() { + Ok(termscp_dir) => match termscp_dir { + Some(termscp_dir) => { + // Make configuration file path and ssh keys path + let (config_path, ssh_keys_path): (PathBuf, PathBuf) = + environment::get_config_paths(termscp_dir.as_path()); + match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) { + Ok(config_client) => config_client, + Err(_) => ConfigClient::degraded(), + } + } + None => ConfigClient::degraded(), + }, + Err(_) => ConfigClient::degraded(), + } + } + + /// Set text editor to use + pub(in crate::ui::activities::filetransfer) fn setup_text_editor(&self) { + unsafe { + env::set_var("EDITOR", self.config().get_text_editor()); + } + } + + /// Convert a path to absolute according to the current tab's pane explorer + pub(in crate::ui::activities::filetransfer) fn pane_to_abs_path(&self, path: &Path) -> PathBuf { + path::absolutize(self.browser.fs_pane().explorer.wrkdir.as_path(), path) + } + + /// Get remote hostname + pub(in crate::ui::activities::filetransfer) fn get_remote_hostname(&self) -> String { + let ft_params = self.context().remote_params().unwrap(); + self.get_hostname(&ft_params.params) + } + + pub(in crate::ui::activities::filetransfer) fn get_hostbridge_hostname(&self) -> String { + let host_bridge_params = self.context().host_bridge_params().unwrap(); + match host_bridge_params { + HostBridgeParams::Localhost(_) => { + let hostname = match hostname::get() { + Ok(h) => h, + Err(_) => return String::from("localhost"), + }; + let hostname: String = hostname.as_os_str().to_string_lossy().to_string(); + let tokens: Vec<&str> = hostname.split('.').collect(); + String::from(*tokens.first().unwrap_or(&"localhost")) + } + HostBridgeParams::Remote(_, params) => self.get_hostname(params), + } + } + + fn get_hostname(&self, params: &ProtocolParams) -> String { + match params { + ProtocolParams::Generic(params) => params.address.clone(), + ProtocolParams::AwsS3(params) => params.bucket_name.clone(), + ProtocolParams::Kube(params) => { + params.namespace.clone().unwrap_or("default".to_string()) + } + ProtocolParams::Smb(params) => params.address.clone(), + ProtocolParams::WebDAV(params) => params.uri.clone(), + } + } + + /// Get connection message to show to client + pub(in crate::ui::activities::filetransfer) fn get_connection_msg( + params: &ProtocolParams, + ) -> String { + match params { + ProtocolParams::Generic(params) => { + info!( + "Client is not connected to remote; connecting to {}:{}", + params.address, params.port + ); + format!("Connecting to {}:{}…", params.address, params.port) + } + ProtocolParams::AwsS3(params) => { + info!( + "Client is not connected to remote; connecting to {}{} ({})", + params.endpoint.as_deref().unwrap_or(""), + params.bucket_name, + params.region.as_deref().unwrap_or("custom") + ); + format!("Connecting to {}…", params.bucket_name) + } + ProtocolParams::Kube(params) => { + let namespace = params.namespace.as_deref().unwrap_or("default"); + info!("Client is not connected to remote; connecting to namespace {namespace}",); + format!("Connecting to Kube namespace {namespace}…",) + } + ProtocolParams::Smb(params) => { + info!( + "Client is not connected to remote; connecting to {}:{}", + params.address, params.share + ); + format!("Connecting to \\\\{}\\{}…", params.address, params.share) + } + ProtocolParams::WebDAV(params) => { + info!( + "Client is not connected to remote; connecting to {}", + params.uri + ); + format!("Connecting to {}…", params.uri) + } + } + } +} diff --git a/src/ui/activities/filetransfer/misc/log.rs b/src/ui/activities/filetransfer/misc/log.rs new file mode 100644 index 0000000..7bb381b --- /dev/null +++ b/src/ui/activities/filetransfer/misc/log.rs @@ -0,0 +1,84 @@ +use tuirealm::props::{AttrValue, Attribute, Color, TableBuilder, TextSpan}; + +use super::super::{FileTransferActivity, Id, LogLevel, LogRecord, ui_result}; + +const LOG_CAPACITY: usize = 256; + +impl FileTransferActivity { + /// Add message to log events + pub(in crate::ui::activities::filetransfer) fn log(&mut self, level: LogLevel, msg: String) { + // Log to file + match level { + LogLevel::Error => error!("{}", msg), + LogLevel::Info => info!("{}", msg), + LogLevel::Warn => warn!("{}", msg), + } + // Create log record + let record: LogRecord = LogRecord::new(level, msg); + //Check if history overflows the size + if self.log_records.len() + 1 > LOG_CAPACITY { + self.log_records.pop_back(); // Start cleaning events from back + } + // Eventually push front the new record + self.log_records.push_front(record); + // Update log + self.update_logbox(); + // flag redraw + self.redraw = true; + } + + /// Add message to log events and also display it as an alert + pub(in crate::ui::activities::filetransfer) fn log_and_alert( + &mut self, + level: LogLevel, + msg: String, + ) { + self.mount_error(msg.as_str()); + self.log(level, msg); + // Update log + self.update_logbox(); + } + + /// Update log box + pub(in crate::ui::activities::filetransfer) fn update_logbox(&mut self) { + let mut table: TableBuilder = TableBuilder::default(); + for (idx, record) in self.log_records.iter().enumerate() { + // Add row if not first row + if idx > 0 { + table.add_row(); + } + let fg = match record.level { + LogLevel::Error => Color::Red, + LogLevel::Warn => Color::Yellow, + LogLevel::Info => Color::Green, + }; + table + .add_col(TextSpan::from(format!( + "{}", + record.time.format("%Y-%m-%dT%H:%M:%S%Z") + ))) + .add_col(TextSpan::from(" [")) + .add_col( + TextSpan::new( + format!( + "{:5}", + match record.level { + LogLevel::Error => "ERROR", + LogLevel::Warn => "WARN", + LogLevel::Info => "INFO", + } + ) + .as_str(), + ) + .fg(fg), + ) + .add_col(TextSpan::from("]: ")) + .add_col(TextSpan::from(record.msg.as_str())); + } + ui_result(self.app.attr( + &Id::Log, + Attribute::Content, + AttrValue::Table(table.build()), + )); + } +} diff --git a/src/ui/activities/filetransfer/misc/notify.rs b/src/ui/activities/filetransfer/misc/notify.rs new file mode 100644 index 0000000..ccb4455 --- /dev/null +++ b/src/ui/activities/filetransfer/misc/notify.rs @@ -0,0 +1,67 @@ +use bytesize::ByteSize; + +use super::super::{FileTransferActivity, TransferPayload}; +use crate::system::notifications::Notification; +use crate::utils::fmt::fmt_millis; + +impl FileTransferActivity { + /// Send notification regarding transfer completed + /// The notification is sent only when these conditions are satisfied: + /// + /// - notifications are enabled + /// - transfer size is greater or equal than notification threshold + pub(in crate::ui::activities::filetransfer) fn notify_transfer_completed( + &self, + payload: &TransferPayload, + ) { + if self.config().get_notifications() + && self.config().get_notification_threshold() as usize <= self.transfer.full_size() + { + Notification::transfer_completed(self.transfer_completed_msg(payload)); + } + } + + /// Send notification regarding transfer error + /// The notification is sent only when these conditions are satisfied: + /// + /// - notifications are enabled + /// - transfer size is greater or equal than notification threshold + pub(in crate::ui::activities::filetransfer) fn notify_transfer_error(&self, msg: &str) { + if self.config().get_notifications() + && self.config().get_notification_threshold() as usize <= self.transfer.full_size() + { + Notification::transfer_error(msg); + } + } + + fn transfer_completed_msg(&self, payload: &TransferPayload) -> String { + let transfer_stats = format!( + "took {} seconds; at {}/s", + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), + ); + match payload { + TransferPayload::File(file) => { + format!( + "File \"{}\" has been successfully transferred ({})", + file.name(), + transfer_stats + ) + } + TransferPayload::Any(entry) => { + format!( + "\"{}\" has been successfully transferred ({})", + entry.name(), + transfer_stats + ) + } + TransferPayload::TransferQueue(entries) => { + format!( + "{} files has been successfully transferred ({})", + entries.len(), + transfer_stats + ) + } + } + } +} diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index d0b43e7..463ce1e 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // This module is split into files, cause it's just too big mod actions; @@ -20,10 +20,10 @@ use std::time::Duration; // Includes use chrono::{DateTime, Local}; use lib::browser; -use lib::browser::Browser; +use lib::browser::{Browser, FileExplorerTab}; +use lib::pane::Pane; use lib::transfer::{TransferOpts, TransferStates}; use lib::walkdir::WalkdirStates; -use remotefs::RemoteFs; use session::TransferPayload; use tempfile::TempDir; use tuirealm::{Application, EventListenerCfg, NoUserEvent}; @@ -34,7 +34,7 @@ use crate::explorer::{FileExplorer, FileSorting}; use crate::filetransfer::{ FileTransferParams, HostBridgeBuilder, HostBridgeParams, RemoteFsBuilder, }; -use crate::host::HostBridge; +use crate::host::RemoteBridged; use crate::system::config_client::ConfigClient; use crate::system::watcher::FsWatcher; @@ -237,10 +237,6 @@ pub struct FileTransferActivity { app: Application, /// Whether should redraw UI redraw: bool, - /// Host bridge - host_bridge: Box, - /// Remote host client - client: Box, /// Browser browser: Browser, /// Current log lines @@ -253,10 +249,6 @@ pub struct FileTransferActivity { cache: Option, /// Fs watcher fswatcher: Option, - /// host bridge connected - host_bridge_connected: bool, - /// remote connected once - remote_connected: bool, } impl FileTransferActivity { @@ -272,6 +264,25 @@ impl FileTransferActivity { let host_bridge = HostBridgeBuilder::build(host_bridge_params, &config_client)?; let host_bridge_connected = host_bridge.is_localhost(); let enable_fs_watcher = host_bridge.is_localhost(); + // Build remote client, wrapped as HostBridge via RemoteBridged + let remote_client = RemoteFsBuilder::build( + remote_params.protocol, + remote_params.params.clone(), + &config_client, + )?; + let remote_fs: Box = + Box::new(RemoteBridged::from(remote_client)); + // Build panes + let local_pane = Pane::new( + Browser::build_local_explorer(&config_client), + host_bridge_connected, + host_bridge, + ); + let remote_pane = Pane::new( + Browser::build_remote_explorer(&config_client), + false, + remote_fs, + ); Ok(Self { exit_reason: None, context: None, @@ -281,14 +292,8 @@ impl FileTransferActivity { .crossterm_input_listener(ticks, CROSSTERM_MAX_POLL), ), redraw: true, - host_bridge, - client: RemoteFsBuilder::build( - remote_params.protocol, - remote_params.params.clone(), - &config_client, - )?, - browser: Browser::new(&config_client), - log_records: VecDeque::with_capacity(256), // 256 events is enough I guess + browser: Browser::new(local_pane, remote_pane), + log_records: VecDeque::with_capacity(256), walkdir: WalkdirStates::default(), transfer: TransferStates::default(), cache: TempDir::new().ok(), @@ -297,11 +302,18 @@ impl FileTransferActivity { } else { None }, - host_bridge_connected, - remote_connected: false, }) } + /// Returns `true` when the active tab targets the local side + /// (either the main host-bridge pane or a find-result rooted there). + fn is_local_tab(&self) -> bool { + matches!( + self.browser.tab(), + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge + ) + } + fn host_bridge(&self) -> &FileExplorer { self.browser.host_bridge() } @@ -433,7 +445,7 @@ impl Activity for FileTransferActivity { error!("Failed to enter raw mode: {}", err); } // Get files at current pwd - if self.host_bridge.is_localhost() { + if self.browser.local_pane().fs.is_localhost() { debug!("Reloading host bridge directory"); self.reload_host_bridge_dir(); } @@ -460,9 +472,10 @@ impl Activity for FileTransferActivity { return; } // Check if connected to host bridge (popup must be None, otherwise would try reconnecting in loop in case of error) - if (!self.host_bridge.is_connected() || !self.host_bridge_connected) + if (!self.browser.local_pane_mut().fs.is_connected() + || !self.browser.local_pane().connected) && !self.app.mounted(&Id::FatalPopup) - && !self.host_bridge.is_localhost() + && !self.browser.local_pane().fs.is_localhost() { let host_bridge_params = self.context().host_bridge_params().unwrap(); let ft_params = host_bridge_params.unwrap_protocol_params(); @@ -476,9 +489,10 @@ impl Activity for FileTransferActivity { self.redraw = true; } // Check if connected to remote (popup must be None, otherwise would try reconnecting in loop in case of error) - if (!self.client.is_connected() || !self.remote_connected) + if (!self.browser.remote_pane_mut().fs.is_connected() + || !self.browser.remote_pane().connected) && !self.app.mounted(&Id::FatalPopup) - && self.host_bridge.is_connected() + && self.browser.local_pane_mut().fs.is_connected() { let ftparams = self.context().remote_params().unwrap(); // print params @@ -522,14 +536,21 @@ impl Activity for FileTransferActivity { if let Err(err) = self.context_mut().terminal().clear_screen() { error!("Failed to clear screen: {}", err); } - // Disconnect client - if self.client.is_connected() { - let _ = self.client.disconnect(); + // Disconnect remote + if self.browser.remote_pane_mut().fs.is_connected() { + let _ = self.browser.remote_pane_mut().fs.disconnect(); } - // disconnect host bridge - if self.host_bridge.is_connected() { - let _ = self.host_bridge.disconnect(); + // Disconnect host bridge + if self.browser.local_pane_mut().fs.is_connected() { + let _ = self.browser.local_pane_mut().fs.disconnect(); } self.context.take() } } + +/// Log a UI operation error instead of panicking. +fn ui_result(result: Result) { + if let Err(err) = result { + error!("UI operation failed: {err}"); + } +} diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index d58ac8d..1527cd8 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -1,1359 +1,9 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall -use std::io::{Read, Write}; -use std::path::{Path, PathBuf}; -use std::time::Instant; +mod connection; +mod navigation; +mod transfer; -use bytesize::ByteSize; -use remotefs::fs::{File, Metadata, ReadStream, UnixPex, Welcome, WriteStream}; -use remotefs::{RemoteError, RemoteErrorType, RemoteResult}; -use thiserror::Error; - -use super::{FileTransferActivity, LogLevel}; -use crate::host::HostError; -use crate::utils::fmt::fmt_millis; - -/// Buffer size for remote I/O -const BUFSIZE: usize = 65535; - -/// Describes the reason that caused an error during a file transfer -#[derive(Error, Debug)] -enum TransferErrorReason { - #[error("File transfer aborted")] - Abrupted, - #[error("I/O error on host_bridgehost: {0}")] - HostIoError(std::io::Error), - #[error("Host error: {0}")] - HostError(HostError), - #[error("I/O error on remote: {0}")] - RemoteIoError(std::io::Error), - #[error("File transfer error: {0}")] - FileTransferError(RemoteError), -} - -/// Represents the entity to send or receive during a transfer. -/// - File: describes an individual `File` to send -/// - Any: Can be any kind of `File`, but just one -/// - Many: a list of `File` -#[derive(Debug)] -pub(super) enum TransferPayload { - File(File), - Any(File), - /// List of file with their destination name - TransferQueue(Vec<(File, PathBuf)>), -} - -impl FileTransferActivity { - pub(super) fn connect_to_host_bridge(&mut self) { - let ft_params = self.context().remote_params().unwrap().clone(); - let entry_dir: Option = ft_params.local_path; - // Connect to host bridge - match self.host_bridge.connect() { - Ok(()) => { - self.host_bridge_connected = self.host_bridge.is_connected(); - if !self.host_bridge_connected { - return; - } - - // Log welcome - self.log( - LogLevel::Info, - format!( - "Established connection with '{}'", - self.get_hostbridge_hostname() - ), - ); - - // Try to change directory to entry directory - let mut remote_chdir: Option = None; - if let Some(remote_path) = &entry_dir { - remote_chdir = Some(remote_path.clone()); - } - if let Some(remote_path) = remote_chdir { - self.local_changedir(remote_path.as_path(), false); - } - // Set state to explorer - self.umount_wait(); - self.reload_host_bridge_dir(); - // Update file lists - self.update_host_bridge_filelist(); - } - Err(err) => { - // Set popup fatal error - self.umount_wait(); - self.mount_fatal(err.to_string()); - } - } - } - - /// Connect to remote - pub(super) fn connect_to_remote(&mut self) { - let ft_params = self.context().remote_params().unwrap().clone(); - let entry_dir: Option = ft_params.remote_path; - // Connect to remote - match self.client.connect() { - Ok(Welcome { banner, .. }) => { - self.remote_connected = self.client.is_connected(); - if !self.remote_connected { - return; - } - - if let Some(banner) = banner { - // Log welcome - self.log( - LogLevel::Info, - format!( - "Established connection with '{}': \"{}\"", - self.get_remote_hostname(), - banner - ), - ); - } else { - // Log welcome - self.log( - LogLevel::Info, - format!( - "Established connection with '{}'", - self.get_remote_hostname() - ), - ); - } - // Try to change directory to entry directory - let mut remote_chdir: Option = None; - if let Some(remote_path) = &entry_dir { - remote_chdir = Some(remote_path.clone()); - } - if let Some(remote_path) = remote_chdir { - self.remote_changedir(remote_path.as_path(), false); - } - // Set state to explorer - self.umount_wait(); - self.reload_remote_dir(); - // Update file lists - self.update_host_bridge_filelist(); - self.update_remote_filelist(); - } - Err(err) => { - // Set popup fatal error - self.umount_wait(); - self.mount_fatal(err.to_string()); - } - } - } - - /// disconnect from remote - pub(super) fn disconnect(&mut self) { - let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname()); - // Show popup disconnecting - self.mount_wait(msg.as_str()); - // Disconnect - let _ = self.client.disconnect(); - // Quit - self.exit_reason = Some(super::ExitReason::Disconnect); - } - - /// disconnect from remote and then quit - pub(super) fn disconnect_and_quit(&mut self) { - self.disconnect(); - self.exit_reason = Some(super::ExitReason::Quit); - } - - /// Reload remote directory entries and update browser - pub(super) fn reload_remote_dir(&mut self) { - if !self.remote_connected { - return; - } - // Get current entries - if let Ok(wrkdir) = self.client.pwd() { - self.mount_blocking_wait("Loading remote directory..."); - - let res = self.remote_scan(wrkdir.as_path()); - - self.umount_wait(); - - match res { - Ok(_) => { - self.remote_mut().wrkdir = wrkdir; - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not scan current remote directory: {err}"), - ); - } - } - } - } - - /// Reload host_bridge directory entries and update browser - pub(super) fn reload_host_bridge_dir(&mut self) { - if !self.host_bridge_connected { - return; - } - - self.mount_blocking_wait("Loading host bridge directory..."); - - let wrkdir = match self.host_bridge.pwd() { - Ok(wrkdir) => wrkdir, - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not scan current host bridge directory: {err}"), - ); - return; - } - }; - - let res = self.host_bridge_scan(wrkdir.as_path()); - - self.umount_wait(); - - match res { - Ok(_) => { - self.host_bridge_mut().wrkdir = wrkdir; - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!("Could not scan current host bridge directory: {err}"), - ); - } - } - } - - /// Scan current host bridge directory - fn host_bridge_scan(&mut self, path: &Path) -> Result<(), HostError> { - match self.host_bridge.list_dir(path) { - Ok(files) => { - // Set files and sort (sorting is implicit) - self.host_bridge_mut().set_files(files); - - Ok(()) - } - Err(err) => Err(err), - } - } - - /// Scan current remote directory - fn remote_scan(&mut self, path: &Path) -> RemoteResult<()> { - match self.client.list_dir(path) { - Ok(files) => { - // Set files and sort (sorting is implicit) - self.remote_mut().set_files(files); - Ok(()) - } - Err(err) => Err(err), - } - } - - /// Send fs entry to remote. - /// If dst_name is Some, entry will be saved with a different name. - /// If entry is a directory, this applies to directory only - pub(super) fn filetransfer_send( - &mut self, - payload: TransferPayload, - curr_remote_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Use different method based on payload - let result = match payload { - TransferPayload::Any(ref entry) => { - self.filetransfer_send_any(entry, curr_remote_path, dst_name) - } - TransferPayload::File(ref file) => { - self.filetransfer_send_file(file, curr_remote_path, dst_name) - } - TransferPayload::TransferQueue(ref entries) => { - self.filetransfer_send_transfer_queue(entries) - } - }; - // Notify - match &result { - Ok(_) => { - self.notify_transfer_completed(&payload); - } - Err(e) => { - self.notify_transfer_error(e.as_str()); - } - } - result - } - - /// Send one file to remote at specified path. - fn filetransfer_send_file( - &mut self, - file: &File, - curr_remote_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total size of transfer - let total_transfer_size: usize = file.metadata.size as usize; - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Uploading {}…", file.path.display())); - // Get remote path - let file_name: String = file.name(); - let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); - let remote_file_name: PathBuf = match dst_name { - Some(s) => PathBuf::from(s.as_str()), - None => PathBuf::from(file_name.as_str()), - }; - remote_path.push(remote_file_name); - // Send - let result = self.filetransfer_send_one(file, remote_path.as_path(), file_name); - // Umount progress bar - self.umount_progress_bar(); - // Return result - result.map_err(|x| x.to_string()) - } - - /// Send a `TransferPayload` of type `Any` - fn filetransfer_send_any( - &mut self, - entry: &File, - curr_remote_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total size of transfer - let total_transfer_size: usize = self.get_total_transfer_size_host(entry); - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Uploading {}…", entry.path().display())); - // Send recurse - let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name); - // Umount progress bar - self.umount_progress_bar(); - result - } - - /// Send transfer queue entries to remote - fn filetransfer_send_transfer_queue( - &mut self, - entries: &[(File, PathBuf)], - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total size of transfer - let total_transfer_size: usize = entries - .iter() - .map(|(x, _)| self.get_total_transfer_size_host(x)) - .sum(); - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Uploading {} entries…", entries.len())); - // Send recurse - let result = entries - .iter() - .map(|(x, remote)| self.filetransfer_send_recurse(x, remote, None)) - .find(|x| x.is_err()) - .unwrap_or(Ok(())); - // Umount progress bar - self.umount_progress_bar(); - result - } - - fn filetransfer_send_recurse( - &mut self, - entry: &File, - curr_remote_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Write popup - let file_name = entry.name(); - // Get remote path - let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); - let remote_file_name: PathBuf = match dst_name { - Some(s) => PathBuf::from(s.as_str()), - None => PathBuf::from(file_name.as_str()), - }; - remote_path.push(remote_file_name); - // Match entry - let result: Result<(), String> = if entry.is_dir() { - // Create directory on remote first - match self - .client - .create_dir(remote_path.as_path(), UnixPex::from(0o755)) - { - Ok(_) => { - self.log( - LogLevel::Info, - format!("Created directory \"{}\"", remote_path.display()), - ); - } - Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => { - self.log( - LogLevel::Info, - format!( - "Directory \"{}\" already exists on remote", - remote_path.display() - ), - ); - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Failed to create directory \"{}\": {}", - remote_path.display(), - err - ), - ); - return Err(err.to_string()); - } - } - // Get files in dir - match self.host_bridge.list_dir(entry.path()) { - Ok(entries) => { - // Iterate over files - for entry in entries.iter() { - // If aborted; break - if self.transfer.aborted() { - break; - } - // Send entry; name is always None after first call - self.filetransfer_send_recurse(entry, remote_path.as_path(), None)? - } - Ok(()) - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not scan directory \"{}\": {}", - entry.path().display(), - err - ), - ); - Err(err.to_string()) - } - } - } else { - match self.filetransfer_send_one(entry, remote_path.as_path(), file_name) { - Err(err) => { - // If transfer was abrupted or there was an IO error on remote, remove file - if matches!( - err, - TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_) - ) { - // Stat file on remote and remove it if exists - match self.client.stat(remote_path.as_path()) { - Err(err) => self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - remote_path.display(), - err - ), - ), - Ok(entry) => { - if let Err(err) = self.client.remove_file(entry.path()) { - self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - remote_path.display(), - err - ), - ); - } - } - } - } - Err(err.to_string()) - } - Ok(_) => Ok(()), - } - }; - // Scan dir on remote - self.reload_remote_dir(); - // If aborted; show popup - if self.transfer.aborted() { - // Log abort - self.log_and_alert( - LogLevel::Warn, - format!("Upload aborted for \"{}\"!", entry.path().display()), - ); - } - result - } - - /// Send host_bridge file and write it to remote path - fn filetransfer_send_one( - &mut self, - host_bridge: &File, - remote: &Path, - file_name: String, - ) -> Result<(), TransferErrorReason> { - // Sync file size and attributes before transfer - let metadata = self - .host_bridge - .stat(host_bridge.path.as_path()) - .map_err(TransferErrorReason::HostError) - .map(|x| x.metadata().clone())?; - - if !self.has_remote_file_changed(remote, &metadata) { - self.log( - LogLevel::Info, - format!( - "file {} won't be transferred since hasn't changed", - host_bridge.path().display() - ), - ); - self.transfer.full.update_progress(metadata.size as usize); - return Ok(()); - } - // Upload file - // Try to open host_bridge file - match self.host_bridge.open_file(host_bridge.path.as_path()) { - Ok(host_bridge_read) => match self.client.create(remote, &metadata) { - Ok(rhnd) => self.filetransfer_send_one_with_stream( - host_bridge, - remote, - file_name, - host_bridge_read, - rhnd, - ), - Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => self - .filetransfer_send_one_wno_stream( - host_bridge, - remote, - file_name, - host_bridge_read, - ), - Err(err) => Err(TransferErrorReason::FileTransferError(err)), - }, - Err(err) => Err(TransferErrorReason::HostError(err)), - } - } - - /// Send file to remote using stream - fn filetransfer_send_one_with_stream( - &mut self, - host: &File, - remote: &Path, - file_name: String, - mut reader: Box, - mut writer: WriteStream, - ) -> Result<(), TransferErrorReason> { - // Write file - let file_size = self - .host_bridge - .stat(host.path()) - .map_err(TransferErrorReason::HostError) - .map(|x| x.metadata().size as usize)?; - // Init transfer - self.transfer.partial.init(file_size); - - // Write remote file - let mut total_bytes_written: usize = 0; - let mut last_progress_val: f64 = 0.0; - let mut last_input_event_fetch: Option = None; - // While the entire file hasn't been completely written, - // Or filetransfer has been aborted - while total_bytes_written < file_size && !self.transfer.aborted() { - // Handle input events (each 500ms) or if never fetched before - if last_input_event_fetch.is_none() - || last_input_event_fetch - .unwrap_or_else(Instant::now) - .elapsed() - .as_millis() - >= 500 - { - // Read events - self.tick(); - // Reset instant - last_input_event_fetch = Some(Instant::now()); - } - // Read till you can - let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; - let delta: usize = match reader.read(&mut buffer) { - Ok(bytes_read) => { - total_bytes_written += bytes_read; - if bytes_read == 0 { - continue; - } else { - let mut delta: usize = 0; - while delta < bytes_read { - // Write bytes - match writer.write(&buffer[delta..bytes_read]) { - Ok(bytes) => { - delta += bytes; - } - Err(err) => { - return Err(TransferErrorReason::RemoteIoError(err)); - } - } - } - delta - } - } - Err(err) => { - return Err(TransferErrorReason::HostIoError(err)); - } - }; - // Increase progress - self.transfer.partial.update_progress(delta); - self.transfer.full.update_progress(delta); - // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { - // Draw - self.update_progress_bar(format!("Uploading \"{file_name}\"…")); - self.view(); - last_progress_val = self.transfer.partial.calc_progress(); - } - } - // Finalize stream - if let Err(err) = self.client.on_written(writer) { - self.log( - LogLevel::Warn, - format!("Could not finalize remote stream: \"{err}\""), - ); - } - // if upload was abrupted, return error - if self.transfer.aborted() { - return Err(TransferErrorReason::Abrupted); - } - // set stat - if let Err(err) = self.client.setstat(remote, host.metadata().clone()) { - error!("failed to set stat for {}: {}", remote.display(), err); - } - self.log( - LogLevel::Info, - format!( - "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - host.path.display(), - remote.display(), - fmt_millis(self.transfer.partial.started().elapsed()), - ByteSize(self.transfer.partial.calc_bytes_per_second()), - ), - ); - Ok(()) - } - - /// Send an `File` to remote without using streams. - fn filetransfer_send_one_wno_stream( - &mut self, - host: &File, - remote: &Path, - file_name: String, - reader: Box, - ) -> Result<(), TransferErrorReason> { - // Sync file size and attributes before transfer - let metadata = self - .host_bridge - .stat(host.path.as_path()) - .map_err(TransferErrorReason::HostError) - .map(|x| x.metadata().clone())?; - // Write file - let file_size = self - .host_bridge - .stat(host.path()) - .map_err(TransferErrorReason::HostError) - .map(|x| x.metadata().size as usize)?; - // Init transfer - self.transfer.partial.init(file_size); - - // Draw before - self.update_progress_bar(format!("Uploading \"{file_name}\"…")); - self.view(); - // Send file - if let Err(err) = self.client.create_file(remote, &metadata, reader) { - return Err(TransferErrorReason::FileTransferError(err)); - } - // set stat - if let Err(err) = self.client.setstat(remote, metadata) { - error!("failed to set stat for {}: {}", remote.display(), err); - } - // Set transfer size ok - self.transfer.partial.update_progress(file_size); - self.transfer.full.update_progress(file_size); - // Draw again after - self.update_progress_bar(format!("Uploading \"{file_name}\"…")); - self.view(); - // log and return Ok - self.log( - LogLevel::Info, - format!( - "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - host.path.display(), - remote.display(), - fmt_millis(self.transfer.partial.started().elapsed()), - ByteSize(self.transfer.partial.calc_bytes_per_second()), - ), - ); - Ok(()) - } - - /// Recv fs entry from remote. - /// If dst_name is Some, entry will be saved with a different name. - /// If entry is a directory, this applies to directory only - pub(super) fn filetransfer_recv( - &mut self, - payload: TransferPayload, - host_bridge_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - let result = match payload { - TransferPayload::Any(ref entry) => { - self.filetransfer_recv_any(entry, host_bridge_path, dst_name) - } - TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path), - TransferPayload::TransferQueue(ref entries) => { - self.filetransfer_recv_transfer_queue(entries) - } - }; - // Notify - match &result { - Ok(_) => { - self.notify_transfer_completed(&payload); - } - Err(e) => { - self.notify_transfer_error(e.as_str()); - } - } - result - } - - /// Recv fs entry from remote. - /// If dst_name is Some, entry will be saved with a different name. - /// If entry is a directory, this applies to directory only - fn filetransfer_recv_any( - &mut self, - entry: &File, - host_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total transfer size - let total_transfer_size: usize = self.get_total_transfer_size_remote(entry); - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Downloading {}…", entry.path().display())); - // Receive - let result = self.filetransfer_recv_recurse(entry, host_path, dst_name); - // Umount progress bar - self.umount_progress_bar(); - result - } - - /// Receive a single file from remote. - fn filetransfer_recv_file( - &mut self, - entry: &File, - host_bridge_path: &Path, - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total transfer size - let total_transfer_size: usize = entry.metadata.size as usize; - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Downloading {}…", entry.path.display())); - // Receive - let result = self.filetransfer_recv_one(host_bridge_path, entry, entry.name()); - // Umount progress bar - self.umount_progress_bar(); - // Return result - result.map_err(|x| x.to_string()) - } - - /// Receive transfer queue from remote - fn filetransfer_recv_transfer_queue( - &mut self, - entries: &[(File, PathBuf)], - ) -> Result<(), String> { - // Reset states - self.transfer.reset(); - // Calculate total size of transfer - let total_transfer_size: usize = entries - .iter() - .map(|(x, _)| self.get_total_transfer_size_remote(x)) - .sum(); - self.transfer.full.init(total_transfer_size); - // Mount progress bar - self.mount_progress_bar(format!("Downloading {} entries…", entries.len())); - // Send recurse - let result = entries - .iter() - .map(|(x, path)| self.filetransfer_recv_recurse(x, path, None)) - .find(|x| x.is_err()) - .unwrap_or(Ok(())); - // Umount progress bar - self.umount_progress_bar(); - result - } - - fn filetransfer_recv_recurse( - &mut self, - entry: &File, - host_bridge_path: &Path, - dst_name: Option, - ) -> Result<(), String> { - // Write popup - let file_name = entry.name(); - // Match entry - let result: Result<(), String> = if entry.is_dir() { - // Get dir name - let mut host_bridge_dir_path: PathBuf = PathBuf::from(host_bridge_path); - match dst_name { - Some(name) => host_bridge_dir_path.push(name), - None => host_bridge_dir_path.push(entry.name()), - } - // Create directory on host_bridge - match self - .host_bridge - .mkdir_ex(host_bridge_dir_path.as_path(), true) - { - Ok(_) => { - // Apply file mode to directory - if let Err(err) = self - .host_bridge - .setstat(host_bridge_dir_path.as_path(), entry.metadata()) - { - self.log( - LogLevel::Error, - format!( - "Could not set stat to directory {:?} to \"{}\": {}", - entry.metadata(), - host_bridge_dir_path.display(), - err - ), - ); - } - self.log( - LogLevel::Info, - format!("Created directory \"{}\"", host_bridge_dir_path.display()), - ); - // Get files in dir - match self.client.list_dir(entry.path()) { - Ok(entries) => { - // Iterate over files - for entry in entries.iter() { - // If transfer has been aborted; break - if self.transfer.aborted() { - break; - } - // Receive entry; name is always None after first call - // Local path becomes host_bridge_dir_path - self.filetransfer_recv_recurse( - entry, - host_bridge_dir_path.as_path(), - None, - )? - } - Ok(()) - } - Err(err) => { - self.log_and_alert( - LogLevel::Error, - format!( - "Could not scan directory \"{}\": {}", - entry.path().display(), - err - ), - ); - Err(err.to_string()) - } - } - } - Err(err) => { - self.log( - LogLevel::Error, - format!( - "Failed to create directory \"{}\": {}", - host_bridge_dir_path.display(), - err - ), - ); - Err(err.to_string()) - } - } - } else { - // Get host_bridge file - let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path); - let host_bridge_file_name: String = match dst_name { - Some(n) => n, - None => entry.name(), - }; - host_bridge_file_path.push(host_bridge_file_name.as_str()); - // Download file - if let Err(err) = - self.filetransfer_recv_one(host_bridge_file_path.as_path(), entry, file_name) - { - // If transfer was abrupted or there was an IO error on remote, remove file - if matches!( - err, - TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_) - ) { - // Stat file - match self.host_bridge.stat(host_bridge_file_path.as_path()) { - Err(err) => self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - host_bridge_file_path.display(), - err - ), - ), - Ok(entry) => { - if let Err(err) = self.host_bridge.remove(&entry) { - self.log( - LogLevel::Error, - format!( - "Could not remove created file {}: {}", - host_bridge_file_path.display(), - err - ), - ); - } - } - } - } - Err(err.to_string()) - } else { - Ok(()) - } - }; - // Reload directory on host_bridge - self.reload_host_bridge_dir(); - // if aborted; show alert - if self.transfer.aborted() { - // Log abort - self.log_and_alert( - LogLevel::Warn, - format!("Download aborted for \"{}\"!", entry.path().display()), - ); - } - result - } - - /// Receive file from remote and write it to host_bridge path - fn filetransfer_recv_one( - &mut self, - host_bridge: &Path, - remote: &File, - file_name: String, - ) -> Result<(), TransferErrorReason> { - // check if files are equal (in case, don't transfer) - if !self.has_host_bridge_file_changed(host_bridge, remote) { - self.log( - LogLevel::Info, - format!( - "file {} won't be transferred since hasn't changed", - remote.path().display() - ), - ); - self.transfer - .full - .update_progress(remote.metadata().size as usize); - return Ok(()); - } - - // Try to open host_bridge file - match self.host_bridge.create_file(host_bridge, &remote.metadata) { - Ok(writer) => { - // Download file from remote - match self.client.open(remote.path.as_path()) { - Ok(rhnd) => self.filetransfer_recv_one_with_stream( - host_bridge, - remote, - file_name, - rhnd, - writer, - ), - Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => { - self.filetransfer_recv_one_wno_stream(host_bridge, remote, file_name) - } - Err(err) => Err(TransferErrorReason::FileTransferError(err)), - } - } - Err(err) => Err(TransferErrorReason::HostError(err)), - } - } - - /// Receive an `File` from remote using stream - fn filetransfer_recv_one_with_stream( - &mut self, - host_bridge: &Path, - remote: &File, - file_name: String, - mut reader: ReadStream, - mut writer: Box, - ) -> Result<(), TransferErrorReason> { - let mut total_bytes_written: usize = 0; - // Init transfer - self.transfer.partial.init(remote.metadata.size as usize); - // Write host_bridge file - let mut last_progress_val: f64 = 0.0; - let mut last_input_event_fetch: Option = None; - // While the entire file hasn't been completely read, - // Or filetransfer has been aborted - while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() { - // Handle input events (each 500 ms) or is None - if last_input_event_fetch.is_none() - || last_input_event_fetch - .unwrap_or_else(Instant::now) - .elapsed() - .as_millis() - >= 500 - { - // Read events - self.tick(); - // Reset instant - last_input_event_fetch = Some(Instant::now()); - } - // Read till you can - let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; - let delta: usize = match reader.read(&mut buffer) { - Ok(bytes_read) => { - total_bytes_written += bytes_read; - if bytes_read == 0 { - continue; - } else { - let mut delta: usize = 0; - while delta < bytes_read { - // Write bytes - match writer.write(&buffer[delta..bytes_read]) { - Ok(bytes) => delta += bytes, - Err(err) => { - return Err(TransferErrorReason::HostIoError(err)); - } - } - } - delta - } - } - Err(err) => { - return Err(TransferErrorReason::RemoteIoError(err)); - } - }; - // Set progress - self.transfer.partial.update_progress(delta); - self.transfer.full.update_progress(delta); - // Draw only if a significant progress has been made (performance improvement) - if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { - // Draw - self.update_progress_bar(format!("Downloading \"{file_name}\"")); - self.view(); - last_progress_val = self.transfer.partial.calc_progress(); - } - } - // Finalize stream - if let Err(err) = self.client.on_read(reader) { - self.log( - LogLevel::Warn, - format!("Could not finalize remote stream: \"{err}\""), - ); - } - // If download was abrupted, return Error - if self.transfer.aborted() { - return Err(TransferErrorReason::Abrupted); - } - - // finalize write - self.host_bridge - .finalize_write(writer) - .map_err(TransferErrorReason::HostError)?; - - // Apply file mode to file - if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) { - self.log( - LogLevel::Error, - format!( - "Could not set stat to file {:?} to \"{}\": {}", - remote.metadata(), - host_bridge.display(), - err - ), - ); - } - // Log - self.log( - LogLevel::Info, - format!( - "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - remote.path.display(), - host_bridge.display(), - fmt_millis(self.transfer.partial.started().elapsed()), - ByteSize(self.transfer.partial.calc_bytes_per_second()), - ), - ); - - Ok(()) - } - - /// Receive an `File` from remote without using stream - fn filetransfer_recv_one_wno_stream( - &mut self, - host_bridge: &Path, - remote: &File, - file_name: String, - ) -> Result<(), TransferErrorReason> { - // Open host_bridge file - let reader = self - .host_bridge - .create_file(host_bridge, &remote.metadata) - .map_err(TransferErrorReason::HostError) - .map(Box::new)?; - // Init transfer - self.transfer.partial.init(remote.metadata.size as usize); - // Draw before transfer - self.update_progress_bar(format!("Downloading \"{file_name}\"")); - self.view(); - // recv wno stream - if let Err(err) = self.client.open_file(remote.path.as_path(), reader) { - return Err(TransferErrorReason::FileTransferError(err)); - } - // Update progress at the end - self.transfer - .partial - .update_progress(remote.metadata.size as usize); - self.transfer - .full - .update_progress(remote.metadata.size as usize); - // Draw after transfer - self.update_progress_bar(format!("Downloading \"{file_name}\"")); - self.view(); - // Apply file mode to file - if let Err(err) = self.host_bridge.setstat(host_bridge, remote.metadata()) { - self.log( - LogLevel::Error, - format!( - "Could not set stat to file {:?} to \"{}\": {}", - remote.metadata(), - host_bridge.display(), - err - ), - ); - } - // Log - self.log( - LogLevel::Info, - format!( - "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", - remote.path.display(), - host_bridge.display(), - fmt_millis(self.transfer.partial.started().elapsed()), - ByteSize(self.transfer.partial.calc_bytes_per_second()), - ), - ); - Ok(()) - } - - /// Change directory for host_bridge - pub(super) fn host_bridge_changedir(&mut self, path: &Path, push: bool) { - // Get current directory - let prev_dir: PathBuf = self.host_bridge().wrkdir.clone(); - // Change directory - match self.host_bridge.change_wrkdir(path) { - Ok(_) => { - self.log( - LogLevel::Info, - format!("Changed directory on host_bridge: {}", path.display()), - ); - // Push prev_dir to stack - if push { - self.host_bridge_mut().pushd(prev_dir.as_path()) - } - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not change working directory: {err}"), - ); - } - } - } - - pub(super) fn local_changedir(&mut self, path: &Path, push: bool) { - // Get current directory - let prev_dir: PathBuf = self.host_bridge().wrkdir.clone(); - // Change directory - match self.host_bridge.change_wrkdir(path) { - Ok(_) => { - self.log( - LogLevel::Info, - format!("Changed directory on host bridge: {}", path.display()), - ); - // Update files - self.reload_host_bridge_dir(); - // Push prev_dir to stack - if push { - self.host_bridge_mut().pushd(prev_dir.as_path()) - } - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not change working directory: {err}"), - ); - } - } - } - - pub(super) fn remote_changedir(&mut self, path: &Path, push: bool) { - // Get current directory - let prev_dir: PathBuf = self.remote().wrkdir.clone(); - // Change directory - match self.client.as_mut().change_dir(path) { - Ok(_) => { - self.log( - LogLevel::Info, - format!("Changed directory on remote: {}", path.display()), - ); - // Update files - self.reload_remote_dir(); - // Push prev_dir to stack - if push { - self.remote_mut().pushd(prev_dir.as_path()) - } - } - Err(err) => { - // Report err - self.log_and_alert( - LogLevel::Error, - format!("Could not change working directory: {err}"), - ); - } - } - } - - /// Download provided file as a temporary file - pub(super) fn download_file_as_temp(&mut self, file: &File) -> Result { - let tmpfile: PathBuf = match self.cache.as_ref() { - Some(cache) => { - let mut p: PathBuf = cache.path().to_path_buf(); - p.push(file.name()); - p - } - None => { - return Err(String::from( - "Could not create tempfile: cache not available", - )); - } - }; - // Download file - match self.filetransfer_recv( - TransferPayload::File(file.clone()), - tmpfile.as_path(), - Some(file.name()), - ) { - Err(err) => Err(format!( - "Could not download {} to temporary file: {}", - file.path.display(), - err - )), - Ok(()) => Ok(tmpfile), - } - } - - // -- transfer sizes - - /// Get total size of transfer for host_bridgehost - fn get_total_transfer_size_host(&mut self, entry: &File) -> usize { - // mount message to tell we are calculating size - self.mount_blocking_wait("Calculating transfer size…"); - - let sz = if entry.is_dir() { - // List dir - match self.host_bridge.list_dir(entry.path()) { - Ok(files) => files - .iter() - .map(|x| self.get_total_transfer_size_host(x)) - .sum(), - Err(err) => { - self.log( - LogLevel::Error, - format!( - "Could not list directory {}: {}", - entry.path().display(), - err - ), - ); - 0 - } - } - } else { - entry.metadata.size as usize - }; - self.umount_wait(); - - sz - } - - /// Get total size of transfer for remote host - fn get_total_transfer_size_remote(&mut self, entry: &File) -> usize { - // mount message to tell we are calculating size - self.mount_blocking_wait("Calculating transfer size…"); - - let sz = if entry.is_dir() { - // List directory - match self.client.list_dir(entry.path()) { - Ok(files) => files - .iter() - .map(|x| self.get_total_transfer_size_remote(x)) - .sum(), - Err(err) => { - self.log( - LogLevel::Error, - format!( - "Could not list directory {}: {}", - entry.path().display(), - err - ), - ); - 0 - } - } - } else { - entry.metadata.size as usize - }; - - self.umount_wait(); - - sz - } - - // file changed - - /// Check whether provided file has changed on host_bridge disk, compared to remote file - fn has_host_bridge_file_changed(&mut self, host_bridge: &Path, remote: &File) -> bool { - // check if files are equal (in case, don't transfer) - if let Ok(host_bridge_file) = self.host_bridge.stat(host_bridge) { - host_bridge_file.metadata().modified != remote.metadata().modified - || host_bridge_file.metadata().size != remote.metadata().size - } else { - true - } - } - - /// Checks whether remote file has changed compared to host_bridge file - fn has_remote_file_changed(&mut self, remote: &Path, host_bridge_metadata: &Metadata) -> bool { - // check if files are equal (in case, don't transfer) - if let Ok(remote_file) = self.client.stat(remote) { - host_bridge_metadata.modified != remote_file.metadata().modified - || host_bridge_metadata.size != remote_file.metadata().size - } else { - true - } - } - - // -- file exist - - pub(crate) fn host_bridge_file_exists(&mut self, p: &Path) -> bool { - self.host_bridge.exists(p).unwrap_or_default() - } - - pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool { - self.client.exists(p).unwrap_or_default() - } -} +pub(super) use transfer::TransferPayload; diff --git a/src/ui/activities/filetransfer/session/connection.rs b/src/ui/activities/filetransfer/session/connection.rs new file mode 100644 index 0000000..997c847 --- /dev/null +++ b/src/ui/activities/filetransfer/session/connection.rs @@ -0,0 +1,105 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use std::path::PathBuf; + +use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel}; + +impl FileTransferActivity { + pub(in crate::ui::activities::filetransfer) fn connect_to_host_bridge(&mut self) { + let ft_params = self.context().remote_params().unwrap().clone(); + let entry_dir: Option = ft_params.local_path; + // Connect to host bridge + match self.browser.local_pane_mut().fs.connect() { + Ok(()) => { + let connected = self.browser.local_pane_mut().fs.is_connected(); + self.browser.local_pane_mut().connected = connected; + if !connected { + return; + } + + // Log welcome + self.log( + LogLevel::Info, + format!( + "Established connection with '{}'", + self.get_hostbridge_hostname() + ), + ); + + // Try to change directory to entry directory + if let Some(entry_path) = entry_dir { + self.local_changedir(entry_path.as_path(), false); + } + // Set state to explorer + self.umount_wait(); + self.reload_host_bridge_dir(); + // Update file lists + self.update_host_bridge_filelist(); + } + Err(err) => { + // Set popup fatal error + self.umount_wait(); + self.mount_fatal(err.to_string()); + } + } + } + + /// Connect to remote + pub(in crate::ui::activities::filetransfer) fn connect_to_remote(&mut self) { + let ft_params = self.context().remote_params().unwrap().clone(); + let entry_dir: Option = ft_params.remote_path; + // Connect to remote (banner is lost when using HostBridge API) + match self.browser.remote_pane_mut().fs.connect() { + Ok(()) => { + let connected = self.browser.remote_pane_mut().fs.is_connected(); + self.browser.remote_pane_mut().connected = connected; + if !connected { + return; + } + + // Log welcome + self.log( + LogLevel::Info, + format!( + "Established connection with '{}'", + self.get_remote_hostname() + ), + ); + // Try to change directory to entry directory + if let Some(entry_path) = entry_dir { + self.remote_changedir(entry_path.as_path(), false); + } + // Set state to explorer + self.umount_wait(); + self.reload_remote_dir(); + // Update file lists + self.update_host_bridge_filelist(); + self.update_remote_filelist(); + } + Err(err) => { + // Set popup fatal error + self.umount_wait(); + self.mount_fatal(err.to_string()); + } + } + } + + /// disconnect from remote + pub(in crate::ui::activities::filetransfer) fn disconnect(&mut self) { + let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname()); + // Show popup disconnecting + self.mount_wait(msg.as_str()); + // Disconnect + let _ = self.browser.remote_pane_mut().fs.disconnect(); + // Quit + self.exit_reason = Some(super::super::ExitReason::Disconnect); + } + + /// disconnect from remote and then quit + pub(in crate::ui::activities::filetransfer) fn disconnect_and_quit(&mut self) { + self.disconnect(); + self.exit_reason = Some(super::super::ExitReason::Quit); + } +} diff --git a/src/ui/activities/filetransfer/session/navigation.rs b/src/ui/activities/filetransfer/session/navigation.rs new file mode 100644 index 0000000..e633d67 --- /dev/null +++ b/src/ui/activities/filetransfer/session/navigation.rs @@ -0,0 +1,284 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use std::path::{Path, PathBuf}; + +use remotefs::fs::{File, Metadata}; + +use super::transfer::TransferPayload; +use crate::host::HostError; +use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel}; + +impl FileTransferActivity { + // -- reload directory -- + + /// Reload remote directory entries and update browser + pub(in crate::ui::activities::filetransfer) fn reload_remote_dir(&mut self) { + self.reload_dir_on(false); + } + + /// Reload host_bridge directory entries and update browser + pub(in crate::ui::activities::filetransfer) fn reload_host_bridge_dir(&mut self) { + self.reload_dir_on(true); + } + + /// Reload directory entries for the specified side. + fn reload_dir_on(&mut self, local: bool) { + let pane = if local { + self.browser.local_pane() + } else { + self.browser.remote_pane() + }; + if !pane.connected { + return; + } + + self.mount_blocking_wait("Loading directory..."); + + let pane = if local { + self.browser.local_pane_mut() + } else { + self.browser.remote_pane_mut() + }; + let wrkdir = match pane.fs.pwd() { + Ok(wrkdir) => wrkdir, + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not scan current directory: {err}"), + ); + self.umount_wait(); + return; + } + }; + + let res = self.scan_on(local, wrkdir.as_path()); + + self.umount_wait(); + + match res { + Ok(_) => { + let explorer = if local { + self.host_bridge_mut() + } else { + self.remote_mut() + }; + explorer.wrkdir = wrkdir; + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not scan current directory: {err}"), + ); + } + } + } + + /// Scan a directory on the specified side and update the explorer file list. + fn scan_on(&mut self, local: bool, path: &Path) -> Result<(), HostError> { + let pane = if local { + self.browser.local_pane_mut() + } else { + self.browser.remote_pane_mut() + }; + let files = pane.fs.list_dir(path)?; + + let explorer = if local { + self.host_bridge_mut() + } else { + self.remote_mut() + }; + explorer.set_files(files); + Ok(()) + } + + // -- change directory -- + + /// Change directory on the current tab's pane (no reload). + pub(in crate::ui::activities::filetransfer) fn pane_changedir( + &mut self, + path: &Path, + push: bool, + ) { + let prev_dir: PathBuf = self.browser.fs_pane().explorer.wrkdir.clone(); + match self.browser.fs_pane_mut().fs.change_wrkdir(path) { + Ok(_) => { + self.log( + LogLevel::Info, + format!("Changed directory: {}", path.display()), + ); + if push { + self.browser + .fs_pane_mut() + .explorer + .pushd(prev_dir.as_path()); + } + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not change working directory: {err}"), + ); + } + } + } + + /// Change directory on the local pane and reload. + pub(in crate::ui::activities::filetransfer) fn local_changedir( + &mut self, + path: &Path, + push: bool, + ) { + let prev_dir: PathBuf = self.host_bridge().wrkdir.clone(); + match self.browser.local_pane_mut().fs.change_wrkdir(path) { + Ok(_) => { + self.log( + LogLevel::Info, + format!("Changed directory on host bridge: {}", path.display()), + ); + self.reload_host_bridge_dir(); + if push { + self.host_bridge_mut().pushd(prev_dir.as_path()) + } + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not change working directory: {err}"), + ); + } + } + } + + /// Change directory on the remote pane and reload. + pub(in crate::ui::activities::filetransfer) fn remote_changedir( + &mut self, + path: &Path, + push: bool, + ) { + let prev_dir: PathBuf = self.remote().wrkdir.clone(); + match self.browser.remote_pane_mut().fs.change_wrkdir(path) { + Ok(_) => { + self.log( + LogLevel::Info, + format!("Changed directory on remote: {}", path.display()), + ); + self.reload_remote_dir(); + if push { + self.remote_mut().pushd(prev_dir.as_path()) + } + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!("Could not change working directory: {err}"), + ); + } + } + } + + // -- temporary file download -- + + /// Download provided file as a temporary file + pub(in crate::ui::activities::filetransfer) fn download_file_as_temp( + &mut self, + file: &File, + ) -> Result { + let tmpfile: PathBuf = match self.cache.as_ref() { + Some(cache) => { + let mut p: PathBuf = cache.path().to_path_buf(); + p.push(file.name()); + p + } + None => { + return Err(String::from( + "Could not create tempfile: cache not available", + )); + } + }; + match self.filetransfer_recv( + TransferPayload::File(file.clone()), + tmpfile.as_path(), + Some(file.name()), + ) { + Err(err) => Err(format!( + "Could not download {} to temporary file: {}", + file.path.display(), + err + )), + Ok(()) => Ok(tmpfile), + } + } + + // -- transfer sizes -- + + /// Get total size of transfer for the specified side. + pub(super) fn get_total_transfer_size(&mut self, entry: &File, local: bool) -> usize { + self.mount_blocking_wait("Calculating transfer size…"); + + let sz = if entry.is_dir() { + let list_result = if local { + self.browser.local_pane_mut().fs.list_dir(entry.path()) + } else { + self.browser.remote_pane_mut().fs.list_dir(entry.path()) + }; + match list_result { + Ok(files) => files + .iter() + .map(|x| self.get_total_transfer_size(x, local)) + .sum(), + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Could not list directory {}: {}", + entry.path().display(), + err + ), + ); + 0 + } + } + } else { + entry.metadata.size as usize + }; + + self.umount_wait(); + sz + } + + // -- file changed -- + + /// Check whether a file has changed on the specified side, compared to the given metadata. + pub(super) fn has_file_changed( + &mut self, + path: &Path, + other_metadata: &Metadata, + local: bool, + ) -> bool { + let stat_result = if local { + self.browser.local_pane_mut().fs.stat(path) + } else { + self.browser.remote_pane_mut().fs.stat(path) + }; + if let Ok(file) = stat_result { + other_metadata.modified != file.metadata().modified + || other_metadata.size != file.metadata().size + } else { + true + } + } + + // -- file exist -- + + /// Check whether a file exists on the specified side. + pub(crate) fn file_exists(&mut self, p: &Path, local: bool) -> bool { + let pane = if local { + self.browser.local_pane_mut() + } else { + self.browser.remote_pane_mut() + }; + pane.fs.exists(p).unwrap_or_default() + } +} diff --git a/src/ui/activities/filetransfer/session/transfer.rs b/src/ui/activities/filetransfer/session/transfer.rs new file mode 100644 index 0000000..83354aa --- /dev/null +++ b/src/ui/activities/filetransfer/session/transfer.rs @@ -0,0 +1,841 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::time::Instant; + +use bytesize::ByteSize; +use remotefs::fs::File; +use thiserror::Error; + +use crate::host::HostError; +use crate::ui::activities::filetransfer::{FileTransferActivity, LogLevel}; +use crate::utils::fmt::fmt_millis; + +/// Buffer size for remote I/O +const BUFSIZE: usize = 65535; + +/// Describes the reason that caused an error during a file transfer +#[derive(Error, Debug)] +enum TransferErrorReason { + #[error("File transfer aborted")] + Abrupted, + #[error("I/O error on host_bridge: {0}")] + HostIoError(std::io::Error), + #[error("Host error: {0}")] + HostError(HostError), + #[error("I/O error on remote: {0}")] + RemoteIoError(std::io::Error), + #[error("Remote error: {0}")] + RemoteHostError(HostError), +} + +/// Represents the entity to send or receive during a transfer. +/// - File: describes an individual `File` to send +/// - Any: Can be any kind of `File`, but just one +/// - Many: a list of `File` +#[derive(Debug)] +pub(in crate::ui::activities::filetransfer) enum TransferPayload { + File(File), + Any(File), + /// List of file with their destination name + TransferQueue(Vec<(File, PathBuf)>), +} + +impl FileTransferActivity { + /// Send fs entry to remote. + /// If dst_name is Some, entry will be saved with a different name. + /// If entry is a directory, this applies to directory only + pub(in crate::ui::activities::filetransfer) fn filetransfer_send( + &mut self, + payload: TransferPayload, + curr_remote_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Use different method based on payload + let result = match payload { + TransferPayload::Any(ref entry) => { + self.filetransfer_send_any(entry, curr_remote_path, dst_name) + } + TransferPayload::File(ref file) => { + self.filetransfer_send_file(file, curr_remote_path, dst_name) + } + TransferPayload::TransferQueue(ref entries) => { + self.filetransfer_send_transfer_queue(entries) + } + }; + // Notify + match &result { + Ok(_) => { + self.notify_transfer_completed(&payload); + } + Err(e) => { + self.notify_transfer_error(e.as_str()); + } + } + result + } + + /// Send one file to remote at specified path. + fn filetransfer_send_file( + &mut self, + file: &File, + curr_remote_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total size of transfer + let total_transfer_size: usize = file.metadata.size as usize; + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Uploading {}…", file.path.display())); + // Get remote path + let file_name: String = file.name(); + let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); + let remote_file_name: PathBuf = match dst_name { + Some(s) => PathBuf::from(s.as_str()), + None => PathBuf::from(file_name.as_str()), + }; + remote_path.push(remote_file_name); + // Send + let result = self.filetransfer_send_one(file, remote_path.as_path(), file_name); + // Umount progress bar + self.umount_progress_bar(); + // Return result + result.map_err(|x| x.to_string()) + } + + /// Send a `TransferPayload` of type `Any` + fn filetransfer_send_any( + &mut self, + entry: &File, + curr_remote_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total size of transfer + let total_transfer_size: usize = self.get_total_transfer_size(entry, true); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Uploading {}…", entry.path().display())); + // Send recurse + let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name); + // Umount progress bar + self.umount_progress_bar(); + result + } + + /// Send transfer queue entries to remote + fn filetransfer_send_transfer_queue( + &mut self, + entries: &[(File, PathBuf)], + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total size of transfer + let total_transfer_size: usize = entries + .iter() + .map(|(x, _)| self.get_total_transfer_size(x, true)) + .sum(); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Uploading {} entries…", entries.len())); + // Send recurse + let result = entries + .iter() + .map(|(x, remote)| self.filetransfer_send_recurse(x, remote, None)) + .find(|x| x.is_err()) + .unwrap_or(Ok(())); + // Umount progress bar + self.umount_progress_bar(); + result + } + + fn filetransfer_send_recurse( + &mut self, + entry: &File, + curr_remote_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Write popup + let file_name = entry.name(); + // Get remote path + let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); + let remote_file_name: PathBuf = match dst_name { + Some(s) => PathBuf::from(s.as_str()), + None => PathBuf::from(file_name.as_str()), + }; + remote_path.push(remote_file_name); + // Match entry + let result: Result<(), String> = if entry.is_dir() { + // Create directory on remote first + match self + .browser + .remote_pane_mut() + .fs + .mkdir_ex(remote_path.as_path(), true) + { + Ok(_) => { + self.log( + LogLevel::Info, + format!("Created directory \"{}\"", remote_path.display()), + ); + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Failed to create directory \"{}\": {}", + remote_path.display(), + err + ), + ); + return Err(err.to_string()); + } + } + // Get files in dir + match self.browser.local_pane_mut().fs.list_dir(entry.path()) { + Ok(entries) => { + // Iterate over files + for entry in entries.iter() { + // If aborted; break + if self.transfer.aborted() { + break; + } + // Send entry; name is always None after first call + self.filetransfer_send_recurse(entry, remote_path.as_path(), None)? + } + Ok(()) + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not scan directory \"{}\": {}", + entry.path().display(), + err + ), + ); + Err(err.to_string()) + } + } + } else { + match self.filetransfer_send_one(entry, remote_path.as_path(), file_name) { + Err(err) => { + // If transfer was abrupted or there was an IO error on remote, remove file + if matches!( + err, + TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_) + ) { + // Stat file on remote and remove it if exists + match self + .browser + .remote_pane_mut() + .fs + .stat(remote_path.as_path()) + { + Err(err) => self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + remote_path.display(), + err + ), + ), + Ok(entry) => { + if let Err(err) = self.browser.remote_pane_mut().fs.remove(&entry) { + self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + remote_path.display(), + err + ), + ); + } + } + } + } + Err(err.to_string()) + } + Ok(_) => Ok(()), + } + }; + // Scan dir on remote + self.reload_remote_dir(); + // If aborted; show popup + if self.transfer.aborted() { + // Log abort + self.log_and_alert( + LogLevel::Warn, + format!("Upload aborted for \"{}\"!", entry.path().display()), + ); + } + result + } + + /// Send host_bridge file and write it to remote path + fn filetransfer_send_one( + &mut self, + host_bridge: &File, + remote: &Path, + file_name: String, + ) -> Result<(), TransferErrorReason> { + // Sync file size and attributes before transfer + let metadata = self + .browser + .local_pane_mut() + .fs + .stat(host_bridge.path.as_path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().clone())?; + + if !self.has_file_changed(remote, &metadata, false) { + self.log( + LogLevel::Info, + format!( + "file {} won't be transferred since hasn't changed", + host_bridge.path().display() + ), + ); + self.transfer.full.update_progress(metadata.size as usize); + return Ok(()); + } + // Upload file + // Open host_bridge file for reading + let reader = self + .browser + .local_pane_mut() + .fs + .open_file(host_bridge.path.as_path()) + .map_err(TransferErrorReason::HostError)?; + // Open remote file for writing + let writer = self + .browser + .remote_pane_mut() + .fs + .create_file(remote, &metadata) + .map_err(TransferErrorReason::RemoteHostError)?; + + self.filetransfer_send_one_with_stream(host_bridge, remote, file_name, reader, writer) + } + + /// Send file to remote using stream + fn filetransfer_send_one_with_stream( + &mut self, + host: &File, + remote: &Path, + file_name: String, + mut reader: Box, + mut writer: Box, + ) -> Result<(), TransferErrorReason> { + // Write file + let file_size = self + .browser + .local_pane_mut() + .fs + .stat(host.path()) + .map_err(TransferErrorReason::HostError) + .map(|x| x.metadata().size as usize)?; + // Init transfer + self.transfer.partial.init(file_size); + + // Write remote file + let mut total_bytes_written: usize = 0; + let mut last_progress_val: f64 = 0.0; + let mut last_input_event_fetch: Option = None; + // While the entire file hasn't been completely written, + // Or filetransfer has been aborted + while total_bytes_written < file_size && !self.transfer.aborted() { + // Handle input events (each 500ms) or if never fetched before + if last_input_event_fetch.is_none() + || last_input_event_fetch + .unwrap_or_else(Instant::now) + .elapsed() + .as_millis() + >= 500 + { + // Read events + self.tick(); + // Reset instant + last_input_event_fetch = Some(Instant::now()); + } + // Read till you can + let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; + let delta: usize = match reader.read(&mut buffer) { + Ok(bytes_read) => { + total_bytes_written += bytes_read; + if bytes_read == 0 { + continue; + } else { + let mut delta: usize = 0; + while delta < bytes_read { + // Write bytes + match writer.write(&buffer[delta..bytes_read]) { + Ok(bytes) => { + delta += bytes; + } + Err(err) => { + return Err(TransferErrorReason::RemoteIoError(err)); + } + } + } + delta + } + } + Err(err) => { + return Err(TransferErrorReason::HostIoError(err)); + } + }; + // Increase progress + self.transfer.partial.update_progress(delta); + self.transfer.full.update_progress(delta); + // Draw only if a significant progress has been made (performance improvement) + if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { + // Draw + self.update_progress_bar(format!("Uploading \"{file_name}\"…")); + self.view(); + last_progress_val = self.transfer.partial.calc_progress(); + } + } + // Finalize stream + if let Err(err) = self.browser.remote_pane_mut().fs.finalize_write(writer) { + self.log( + LogLevel::Warn, + format!("Could not finalize remote stream: \"{err}\""), + ); + } + // if upload was abrupted, return error + if self.transfer.aborted() { + return Err(TransferErrorReason::Abrupted); + } + // set stat + if let Err(err) = self + .browser + .remote_pane_mut() + .fs + .setstat(remote, host.metadata()) + { + error!("failed to set stat for {}: {}", remote.display(), err); + } + self.log( + LogLevel::Info, + format!( + "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", + host.path.display(), + remote.display(), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), + ), + ); + Ok(()) + } + + /// Recv fs entry from remote. + /// If dst_name is Some, entry will be saved with a different name. + /// If entry is a directory, this applies to directory only + pub(in crate::ui::activities::filetransfer) fn filetransfer_recv( + &mut self, + payload: TransferPayload, + host_bridge_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + let result = match payload { + TransferPayload::Any(ref entry) => { + self.filetransfer_recv_any(entry, host_bridge_path, dst_name) + } + TransferPayload::File(ref file) => self.filetransfer_recv_file(file, host_bridge_path), + TransferPayload::TransferQueue(ref entries) => { + self.filetransfer_recv_transfer_queue(entries) + } + }; + // Notify + match &result { + Ok(_) => { + self.notify_transfer_completed(&payload); + } + Err(e) => { + self.notify_transfer_error(e.as_str()); + } + } + result + } + + /// Recv fs entry from remote. + /// If dst_name is Some, entry will be saved with a different name. + /// If entry is a directory, this applies to directory only + fn filetransfer_recv_any( + &mut self, + entry: &File, + host_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total transfer size + let total_transfer_size: usize = self.get_total_transfer_size(entry, false); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Downloading {}…", entry.path().display())); + // Receive + let result = self.filetransfer_recv_recurse(entry, host_path, dst_name); + // Umount progress bar + self.umount_progress_bar(); + result + } + + /// Receive a single file from remote. + fn filetransfer_recv_file( + &mut self, + entry: &File, + host_bridge_path: &Path, + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total transfer size + let total_transfer_size: usize = entry.metadata.size as usize; + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Downloading {}…", entry.path.display())); + // Receive + let result = self.filetransfer_recv_one(host_bridge_path, entry, entry.name()); + // Umount progress bar + self.umount_progress_bar(); + // Return result + result.map_err(|x| x.to_string()) + } + + /// Receive transfer queue from remote + fn filetransfer_recv_transfer_queue( + &mut self, + entries: &[(File, PathBuf)], + ) -> Result<(), String> { + // Reset states + self.transfer.reset(); + // Calculate total size of transfer + let total_transfer_size: usize = entries + .iter() + .map(|(x, _)| self.get_total_transfer_size(x, false)) + .sum(); + self.transfer.full.init(total_transfer_size); + // Mount progress bar + self.mount_progress_bar(format!("Downloading {} entries…", entries.len())); + // Send recurse + let result = entries + .iter() + .map(|(x, path)| self.filetransfer_recv_recurse(x, path, None)) + .find(|x| x.is_err()) + .unwrap_or(Ok(())); + // Umount progress bar + self.umount_progress_bar(); + result + } + + fn filetransfer_recv_recurse( + &mut self, + entry: &File, + host_bridge_path: &Path, + dst_name: Option, + ) -> Result<(), String> { + // Write popup + let file_name = entry.name(); + // Match entry + let result: Result<(), String> = if entry.is_dir() { + // Get dir name + let mut host_bridge_dir_path: PathBuf = PathBuf::from(host_bridge_path); + match dst_name { + Some(name) => host_bridge_dir_path.push(name), + None => host_bridge_dir_path.push(entry.name()), + } + // Create directory on host_bridge + match self + .browser + .local_pane_mut() + .fs + .mkdir_ex(host_bridge_dir_path.as_path(), true) + { + Ok(_) => { + // Apply file mode to directory + if let Err(err) = self + .browser + .local_pane_mut() + .fs + .setstat(host_bridge_dir_path.as_path(), entry.metadata()) + { + self.log( + LogLevel::Error, + format!( + "Could not set stat to directory {:?} to \"{}\": {}", + entry.metadata(), + host_bridge_dir_path.display(), + err + ), + ); + } + self.log( + LogLevel::Info, + format!("Created directory \"{}\"", host_bridge_dir_path.display()), + ); + // Get files in dir from remote + match self.browser.remote_pane_mut().fs.list_dir(entry.path()) { + Ok(entries) => { + // Iterate over files + for entry in entries.iter() { + // If transfer has been aborted; break + if self.transfer.aborted() { + break; + } + // Receive entry; name is always None after first call + // Local path becomes host_bridge_dir_path + self.filetransfer_recv_recurse( + entry, + host_bridge_dir_path.as_path(), + None, + )? + } + Ok(()) + } + Err(err) => { + self.log_and_alert( + LogLevel::Error, + format!( + "Could not scan directory \"{}\": {}", + entry.path().display(), + err + ), + ); + Err(err.to_string()) + } + } + } + Err(err) => { + self.log( + LogLevel::Error, + format!( + "Failed to create directory \"{}\": {}", + host_bridge_dir_path.display(), + err + ), + ); + Err(err.to_string()) + } + } + } else { + // Get host_bridge file + let mut host_bridge_file_path: PathBuf = PathBuf::from(host_bridge_path); + let host_bridge_file_name: String = match dst_name { + Some(n) => n, + None => entry.name(), + }; + host_bridge_file_path.push(host_bridge_file_name.as_str()); + // Download file + if let Err(err) = + self.filetransfer_recv_one(host_bridge_file_path.as_path(), entry, file_name) + { + // If transfer was abrupted or there was an IO error on remote, remove file + if matches!( + err, + TransferErrorReason::Abrupted | TransferErrorReason::HostIoError(_) + ) { + // Stat file + match self + .browser + .local_pane_mut() + .fs + .stat(host_bridge_file_path.as_path()) + { + Err(err) => self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + host_bridge_file_path.display(), + err + ), + ), + Ok(entry) => { + if let Err(err) = self.browser.local_pane_mut().fs.remove(&entry) { + self.log( + LogLevel::Error, + format!( + "Could not remove created file {}: {}", + host_bridge_file_path.display(), + err + ), + ); + } + } + } + } + Err(err.to_string()) + } else { + Ok(()) + } + }; + // Reload directory on host_bridge + self.reload_host_bridge_dir(); + // if aborted; show alert + if self.transfer.aborted() { + // Log abort + self.log_and_alert( + LogLevel::Warn, + format!("Download aborted for \"{}\"!", entry.path().display()), + ); + } + result + } + + /// Receive file from remote and write it to host_bridge path + fn filetransfer_recv_one( + &mut self, + host_bridge: &Path, + remote: &File, + file_name: String, + ) -> Result<(), TransferErrorReason> { + // check if files are equal (in case, don't transfer) + if !self.has_file_changed(host_bridge, remote.metadata(), true) { + self.log( + LogLevel::Info, + format!( + "file {} won't be transferred since hasn't changed", + remote.path().display() + ), + ); + self.transfer + .full + .update_progress(remote.metadata().size as usize); + return Ok(()); + } + + // Open host_bridge file for writing + let writer = self + .browser + .local_pane_mut() + .fs + .create_file(host_bridge, &remote.metadata) + .map_err(TransferErrorReason::HostError)?; + // Open remote file for reading + let reader = self + .browser + .remote_pane_mut() + .fs + .open_file(remote.path.as_path()) + .map_err(TransferErrorReason::RemoteHostError)?; + + self.filetransfer_recv_one_with_stream(host_bridge, remote, file_name, reader, writer) + } + + /// Receive an `File` from remote using stream + fn filetransfer_recv_one_with_stream( + &mut self, + host_bridge: &Path, + remote: &File, + file_name: String, + mut reader: Box, + mut writer: Box, + ) -> Result<(), TransferErrorReason> { + let mut total_bytes_written: usize = 0; + // Init transfer + self.transfer.partial.init(remote.metadata.size as usize); + // Write host_bridge file + let mut last_progress_val: f64 = 0.0; + let mut last_input_event_fetch: Option = None; + // While the entire file hasn't been completely read, + // Or filetransfer has been aborted + while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() { + // Handle input events (each 500 ms) or is None + if last_input_event_fetch.is_none() + || last_input_event_fetch + .unwrap_or_else(Instant::now) + .elapsed() + .as_millis() + >= 500 + { + // Read events + self.tick(); + // Reset instant + last_input_event_fetch = Some(Instant::now()); + } + // Read till you can + let mut buffer: [u8; BUFSIZE] = [0; BUFSIZE]; + let delta: usize = match reader.read(&mut buffer) { + Ok(bytes_read) => { + total_bytes_written += bytes_read; + if bytes_read == 0 { + continue; + } else { + let mut delta: usize = 0; + while delta < bytes_read { + // Write bytes + match writer.write(&buffer[delta..bytes_read]) { + Ok(bytes) => delta += bytes, + Err(err) => { + return Err(TransferErrorReason::HostIoError(err)); + } + } + } + delta + } + } + Err(err) => { + return Err(TransferErrorReason::RemoteIoError(err)); + } + }; + // Set progress + self.transfer.partial.update_progress(delta); + self.transfer.full.update_progress(delta); + // Draw only if a significant progress has been made (performance improvement) + if last_progress_val < self.transfer.partial.calc_progress() - 0.01 { + // Draw + self.update_progress_bar(format!("Downloading \"{file_name}\"")); + self.view(); + last_progress_val = self.transfer.partial.calc_progress(); + } + } + // If download was abrupted, return Error + if self.transfer.aborted() { + return Err(TransferErrorReason::Abrupted); + } + + // Finalize write + self.browser + .local_pane_mut() + .fs + .finalize_write(writer) + .map_err(TransferErrorReason::HostError)?; + + // Apply file mode to file + if let Err(err) = self + .browser + .local_pane_mut() + .fs + .setstat(host_bridge, remote.metadata()) + { + self.log( + LogLevel::Error, + format!( + "Could not set stat to file {:?} to \"{}\": {}", + remote.metadata(), + host_bridge.display(), + err + ), + ); + } + // Log + self.log( + LogLevel::Info, + format!( + "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", + remote.path.display(), + host_bridge.display(), + fmt_millis(self.transfer.partial.started().elapsed()), + ByteSize(self.transfer.partial.calc_bytes_per_second()), + ), + ); + + Ok(()) + } +} diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index bce542c..180c741 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -1,6 +1,6 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall // locals // externals @@ -12,6 +12,7 @@ use super::actions::walkdir::WalkdirError; use super::browser::{FileExplorerTab, FoundExplorerTab}; use super::{ ExitReason, FileTransferActivity, Id, MarkQueue, Msg, TransferMsg, TransferOpts, UiMsg, + ui_result, }; impl Update for FileTransferActivity { @@ -40,13 +41,12 @@ impl FileTransferActivity { TransferMsg::Chmod(mode) => { self.umount_chmod(); self.mount_blocking_wait("Applying new file mode…"); - match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge - if self.host_bridge.is_localhost() && cfg!(windows) => {} - FileExplorerTab::HostBridge => self.action_local_chmod(mode), - FileExplorerTab::FindHostBridge => self.action_find_local_chmod(mode), - FileExplorerTab::Remote => self.action_remote_chmod(mode), - FileExplorerTab::FindRemote => self.action_find_remote_chmod(mode), + // Skip chmod on Windows localhost + if !(self.is_local_tab() + && self.browser.local_pane().fs.is_localhost() + && cfg!(windows)) + { + self.action_chmod(mode); } self.umount_wait(); self.update_browser_file_list(); @@ -54,11 +54,7 @@ impl FileTransferActivity { TransferMsg::CopyFileTo(dest) => { self.umount_copy(); self.mount_blocking_wait("Copying file(s)…"); - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_copy(dest), - FileExplorerTab::Remote => self.action_remote_copy(dest), - _ => panic!("Found tab doesn't support COPY"), - } + self.action_copy(dest); self.umount_wait(); // Reload files self.update_browser_file_list() @@ -66,11 +62,7 @@ impl FileTransferActivity { TransferMsg::CreateSymlink(name) => { self.umount_symlink(); self.mount_blocking_wait("Creating symlink…"); - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_symlink(name), - FileExplorerTab::Remote => self.action_remote_symlink(name), - _ => panic!("Found tab doesn't support SYMLINK"), - } + self.action_symlink(name); self.umount_wait(); // Reload files self.update_browser_file_list() @@ -79,15 +71,14 @@ impl FileTransferActivity { self.umount_radio_delete(); self.mount_blocking_wait("Removing file(s)…"); match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_delete(), - FileExplorerTab::Remote => self.action_remote_delete(), + FileExplorerTab::HostBridge | FileExplorerTab::Remote => { + self.action_delete(); + } FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { - // Get entry self.action_find_delete(); - // Delete entries + // Remove deleted entries from the find-result list match self.app.state(&Id::ExplorerFind) { Ok(State::One(StateValue::Usize(idx))) => { - // Reload entries self.found_mut().unwrap().del_entry(idx); } Ok(State::Vec(values)) => { @@ -106,31 +97,19 @@ impl FileTransferActivity { } self.umount_wait(); // Reload files - match self.browser.tab() { - FileExplorerTab::HostBridge => self.update_host_bridge_filelist(), - FileExplorerTab::Remote => self.update_remote_filelist(), - FileExplorerTab::FindHostBridge => self.update_host_bridge_filelist(), - FileExplorerTab::FindRemote => self.update_remote_filelist(), - } + self.update_browser_file_list(); } - TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::HostBridge => { - if let Some(entry) = self.get_local_selected_file() { - self.action_submit_local(entry); + TransferMsg::EnterDirectory + if self.browser.tab() == FileExplorerTab::HostBridge + || self.browser.tab() == FileExplorerTab::Remote => + { + if let Some(entry) = self.get_selected_file() { + self.action_submit(entry); // Update file list if sync if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_remote_filelist(); + self.update_browser_file_list_swapped(); } - self.update_host_bridge_filelist(); - } - } - TransferMsg::EnterDirectory if self.browser.tab() == FileExplorerTab::Remote => { - if let Some(entry) = self.get_remote_selected_file() { - self.action_submit_remote(entry); - // Update file list if sync - if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_host_bridge_filelist(); - } - self.update_remote_filelist(); + self.update_browser_file_list(); } } TransferMsg::EnterDirectory => { @@ -145,22 +124,13 @@ impl FileTransferActivity { self.update_browser_file_list() } TransferMsg::ExecuteCmd(cmd) => { - // Exec command - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_exec(cmd), - FileExplorerTab::Remote => self.action_remote_exec(cmd), - _ => panic!("Found tab doesn't support EXEC"), - }; + self.action_exec_cmd(cmd); } TransferMsg::GetFileSize => { self.action_get_file_size(); } TransferMsg::GoTo(dir) => { - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_change_local_dir(dir), - FileExplorerTab::Remote => self.action_change_remote_dir(dir), - _ => panic!("Found tab doesn't support GOTO"), - } + self.action_change_dir(dir); // Umount self.umount_goto(); // Reload files if sync @@ -171,56 +141,24 @@ impl FileTransferActivity { self.update_browser_file_list() } TransferMsg::GoToParentDirectory => { - match self.browser.tab() { - FileExplorerTab::HostBridge => { - self.action_go_to_local_upper_dir(); - if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_remote_filelist(); - } - // Reload file list component - self.update_host_bridge_filelist() - } - FileExplorerTab::Remote => { - self.action_go_to_remote_upper_dir(); - if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_host_bridge_filelist(); - } - // Reload file list component - self.update_remote_filelist() - } - _ => {} + self.action_go_to_upper_dir(); + if self.browser.sync_browsing && self.browser.found().is_none() { + self.update_browser_file_list_swapped(); } + self.update_browser_file_list(); } TransferMsg::GoToPreviousDirectory => { - match self.browser.tab() { - FileExplorerTab::HostBridge => { - self.action_go_to_previous_local_dir(); - if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_remote_filelist(); - } - // Reload file list component - self.update_host_bridge_filelist() - } - FileExplorerTab::Remote => { - self.action_go_to_previous_remote_dir(); - if self.browser.sync_browsing && self.browser.found().is_none() { - self.update_host_bridge_filelist(); - } - // Reload file list component - self.update_remote_filelist() - } - _ => {} + self.action_go_to_previous_dir(); + if self.browser.sync_browsing && self.browser.found().is_none() { + self.update_browser_file_list_swapped(); } + self.update_browser_file_list(); } TransferMsg::InitFuzzySearch => { // Mount wait self.mount_walkdir_wait(); // Find - let res: Result, WalkdirError> = match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_walkdir_local(), - FileExplorerTab::Remote => self.action_walkdir_remote(), - _ => panic!("Trying to search for files, while already in a find result"), - }; + let res: Result, WalkdirError> = self.action_walkdir(); // Umount wait self.umount_wait(); // Match result @@ -266,36 +204,28 @@ impl FileTransferActivity { } } TransferMsg::Mkdir(dir) => { - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_mkdir(dir), - FileExplorerTab::Remote => self.action_remote_mkdir(dir), - _ => {} - } + self.action_mkdir(dir); self.umount_mkdir(); // Reload files self.update_browser_file_list() } TransferMsg::NewFile(name) => { - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_newfile(name), - FileExplorerTab::Remote => self.action_remote_newfile(name), - _ => {} - } + self.action_newfile(name); self.umount_newfile(); // Reload files self.update_browser_file_list() } TransferMsg::OpenFile => match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_open_local(), - FileExplorerTab::Remote => self.action_open_remote(), + FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.action_open(), FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.action_find_open() } }, TransferMsg::OpenFileWith(prog) => { match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_open_with(&prog), - FileExplorerTab::Remote => self.action_remote_open_with(&prog), + FileExplorerTab::HostBridge | FileExplorerTab::Remote => { + self.action_open_with(&prog) + } FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.action_find_open_with(&prog) } @@ -314,11 +244,7 @@ impl FileTransferActivity { TransferMsg::RenameFile(dest) => { self.umount_rename(); self.mount_blocking_wait("Moving file(s)…"); - match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_rename(dest), - FileExplorerTab::Remote => self.action_remote_rename(dest), - _ => {} - } + self.action_rename(dest); self.umount_wait(); // Reload files self.update_browser_file_list() @@ -335,10 +261,10 @@ impl FileTransferActivity { TransferMsg::SaveFileAs(dest) => { self.umount_saveas(); match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_saveas(dest), - FileExplorerTab::Remote => self.action_remote_saveas(dest), + FileExplorerTab::HostBridge | FileExplorerTab::Remote => { + self.action_saveas(dest) + } FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { - // Get entry self.action_find_transfer(TransferOpts::default().save_as(Some(dest))); } } @@ -351,8 +277,9 @@ impl FileTransferActivity { TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index), TransferMsg::TransferFile => { match self.browser.tab() { - FileExplorerTab::HostBridge => self.action_local_send(), - FileExplorerTab::Remote => self.action_remote_recv(), + FileExplorerTab::HostBridge | FileExplorerTab::Remote => { + self.action_transfer_file() + } FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { self.action_find_transfer(TransferOpts::default()) } @@ -399,13 +326,13 @@ impl FileTransferActivity { // Set focus match new_tab { FileExplorerTab::HostBridge => { - assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()) + ui_result(self.app.active(&Id::ExplorerHostBridge)); } FileExplorerTab::Remote => { - assert!(self.app.active(&Id::ExplorerRemote).is_ok()) + ui_result(self.app.active(&Id::ExplorerRemote)); } FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => { - assert!(self.app.active(&Id::ExplorerFind).is_ok()) + ui_result(self.app.active(&Id::ExplorerFind)); } } self.browser.change_tab(new_tab); @@ -476,10 +403,10 @@ impl FileTransferActivity { self.update_find_list(); } UiMsg::GoToTransferQueue => { - assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok()); + ui_result(self.app.active(&Id::TransferQueueHostBridge)); } UiMsg::LogBackTabbed => { - assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()); + ui_result(self.app.active(&Id::ExplorerHostBridge)); } UiMsg::MarkFile(index) => { self.action_mark_file(index); @@ -507,18 +434,16 @@ impl FileTransferActivity { self.umount_quit(); } UiMsg::ShowChmodPopup => { - let selected_file = match self.browser.tab() { - #[cfg(posix)] - FileExplorerTab::HostBridge => self.get_local_selected_entries(), - #[cfg(posix)] - FileExplorerTab::FindHostBridge => self.get_found_selected_entries(), - FileExplorerTab::Remote => self.get_remote_selected_entries(), - FileExplorerTab::FindRemote => self.get_found_selected_entries(), - #[cfg(win)] - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => { - SelectedFile::None - } + // On Windows localhost, chmod is not supported + #[cfg(win)] + let selected_file = if self.is_local_tab() { + SelectedFile::None + } else { + self.get_selected_entries() }; + #[cfg(posix)] + let selected_file = self.get_selected_entries(); + if let Some(mode) = selected_file.unix_pex() { self.mount_chmod( mode, @@ -541,18 +466,8 @@ impl FileTransferActivity { self.browser.toggle_terminal(true); self.mount_exec() } - UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::HostBridge => { - if let SelectedFile::One(file) = self.get_local_selected_entries() { - self.mount_file_info(&file); - } - } - UiMsg::ShowFileInfoPopup if self.browser.tab() == FileExplorerTab::Remote => { - if let SelectedFile::One(file) = self.get_remote_selected_entries() { - self.mount_file_info(&file); - } - } UiMsg::ShowFileInfoPopup => { - if let SelectedFile::One(file) = self.get_found_selected_entries() { + if let SelectedFile::One(file) = self.get_selected_entries() { self.mount_file_info(&file); } } @@ -567,12 +482,12 @@ impl FileTransferActivity { UiMsg::ShowRenamePopup => self.mount_rename(), UiMsg::ShowSaveAsPopup => self.mount_saveas(), UiMsg::ShowSymlinkPopup => { - if match self.browser.tab() { - FileExplorerTab::HostBridge => self.is_local_selected_one(), - FileExplorerTab::Remote => self.is_remote_selected_one(), + // Symlink is not available from find-result tabs + let can_symlink = match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::Remote => self.is_selected_one(), FileExplorerTab::FindHostBridge | FileExplorerTab::FindRemote => false, - } { - // Only if only one entry is selected + }; + if can_symlink { self.mount_symlink(); } else { self.mount_error( @@ -604,25 +519,25 @@ impl FileTransferActivity { UiMsg::BottomPanelLeft => match self.app.focus() { Some(Id::TransferQueueHostBridge) => { - assert!(self.app.active(&Id::Log).is_ok()) + ui_result(self.app.active(&Id::Log)); } Some(Id::TransferQueueRemote) => { - assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok()) + ui_result(self.app.active(&Id::TransferQueueHostBridge)); } Some(Id::Log) => { - assert!(self.app.active(&Id::TransferQueueRemote).is_ok()) + ui_result(self.app.active(&Id::TransferQueueRemote)); } _ => {} }, UiMsg::BottomPanelRight => match self.app.focus() { Some(Id::TransferQueueHostBridge) => { - assert!(self.app.active(&Id::TransferQueueRemote).is_ok()) + ui_result(self.app.active(&Id::TransferQueueRemote)); } Some(Id::TransferQueueRemote) => { - assert!(self.app.active(&Id::Log).is_ok()) + ui_result(self.app.active(&Id::Log)); } Some(Id::Log) => { - assert!(self.app.active(&Id::TransferQueueHostBridge).is_ok()) + ui_result(self.app.active(&Id::TransferQueueHostBridge)); } _ => {} }, diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 0842285..5e6b998 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -1,1211 +1,7 @@ //! ## FileTransferActivity //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall -// locals -// Ext -use remotefs::fs::{File, UnixPex}; -use tuirealm::event::{Key, KeyEvent, KeyModifiers}; -use tuirealm::props::{PropPayload, PropValue, TextSpan}; -use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; -use tuirealm::ratatui::widgets::Clear; -use tuirealm::{AttrValue, Attribute, Sub, SubClause, SubEventClause}; -use unicode_width::UnicodeWidthStr; - -use super::browser::{FileExplorerTab, FoundExplorerTab}; -use super::components::ATTR_FILES; -use super::{Context, FileTransferActivity, Id, components}; -use crate::explorer::FileSorting; -use crate::ui::activities::filetransfer::MarkQueue; -use crate::utils::ui::{Popup, Size}; - -impl FileTransferActivity { - // -- init - - /// Initialize file transfer activity's view - pub(super) fn init(&mut self) { - // Mount local file explorer - let local_explorer_background = self.theme().transfer_local_explorer_background; - let local_explorer_foreground = self.theme().transfer_local_explorer_foreground; - let local_explorer_highlighted = self.theme().transfer_local_explorer_highlighted; - let remote_explorer_background = self.theme().transfer_remote_explorer_background; - let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground; - let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted; - let key_color = self.theme().misc_keys; - let log_panel = self.theme().transfer_log_window; - let log_background = self.theme().transfer_log_background; - assert!( - self.app - .mount( - Id::FooterBar, - Box::new(components::FooterBar::new(key_color)), - vec![] - ) - .is_ok() - ); - assert!( - self.app - .mount( - Id::ExplorerHostBridge, - Box::new(components::ExplorerLocal::new( - "", - &[], - local_explorer_background, - local_explorer_foreground, - local_explorer_highlighted - )), - vec![] - ) - .is_ok() - ); - assert!( - self.app - .mount( - Id::ExplorerRemote, - Box::new(components::ExplorerRemote::new( - "", - &[], - remote_explorer_background, - remote_explorer_foreground, - remote_explorer_highlighted - )), - vec![] - ) - .is_ok() - ); - assert!( - self.app - .mount( - Id::Log, - Box::new(components::Log::new(vec![], log_panel, log_background)), - vec![] - ) - .is_ok() - ); - self.refresh_host_bridge_transfer_queue(); - self.refresh_remote_transfer_queue(); - // Load status bar - self.refresh_local_status_bar(); - self.refresh_remote_status_bar(); - // Update components - self.update_host_bridge_filelist(); - // self.update_remote_filelist(); - // Global listener - self.mount_global_listener(); - // Give focus to local explorer - assert!(self.app.active(&Id::ExplorerHostBridge).is_ok()); - } - - // -- view - - /// View gui - pub(super) fn view(&mut self) { - self.redraw = false; - let mut context: Context = self.context.take().unwrap(); - let _ = context.terminal.raw_mut().draw(|f| { - // Prepare chunks - let body = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Min(7), // Body - Constraint::Length(1), // Footer - ] - .as_ref(), - ) - .split(f.area()); - // main chunks - let main_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(70), // Explorer - Constraint::Percentage(30), // Log - ] - .as_ref(), - ) - .split(body[0]); - // Create explorer chunks - let tabs_chunks = Layout::default() - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .direction(Direction::Horizontal) - .split(main_chunks[0]); - // Create log box chunks - let bottom_chunks = Layout::default() - .constraints([Constraint::Length(1), Constraint::Length(10)].as_ref()) - .direction(Direction::Vertical) - .split(main_chunks[1]); - // Create status bar chunks - let status_bar_chunks = Layout::default() - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .direction(Direction::Horizontal) - .horizontal_margin(1) - .split(bottom_chunks[0]); - let bottom_components = Layout::default() - .constraints( - [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(50), - ] - .as_ref(), - ) - .direction(Direction::Horizontal) - .split(bottom_chunks[1]); - // Draw footer - self.app.view(&Id::FooterBar, f, body[1]); - // Draw explorers - // @! Local explorer (Find or default) - if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { - self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); - } else if self.browser.is_terminal_open_host_bridge() { - self.app.view(&Id::TerminalHostBridge, f, tabs_chunks[0]); - } else { - self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]); - } - // @! Remote explorer (Find or default) - if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { - self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]); - } else if self.browser.is_terminal_open_remote() { - self.app.view(&Id::TerminalRemote, f, tabs_chunks[1]); - } else { - self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]); - } - // draw transfer queues - self.app - .view(&Id::TransferQueueHostBridge, f, bottom_components[0]); - self.app - .view(&Id::TransferQueueRemote, f, bottom_components[1]); - // Draw log box - self.app.view(&Id::Log, f, bottom_components[2]); - // Draw status bar - self.app - .view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]); - self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); - // @! Draw popups - if self.app.mounted(&Id::FatalPopup) { - let popup = Popup( - Size::Percentage(50), - self.calc_popup_height(Id::FatalPopup, f.area().width, f.area().height), - ) - .draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::FatalPopup, f, popup); - } else if self.app.mounted(&Id::CopyPopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::CopyPopup, f, popup); - } else if self.app.mounted(&Id::ChmodPopup) { - let popup = Popup(Size::Percentage(50), Size::Unit(12)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::ChmodPopup, f, popup); - } else if self.app.mounted(&Id::FilterPopup) { - let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::FilterPopup, f, popup); - } else if self.app.mounted(&Id::GotoPopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::GotoPopup, f, popup); - } else if self.app.mounted(&Id::MkdirPopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::MkdirPopup, f, popup); - } else if self.app.mounted(&Id::NewfilePopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::NewfilePopup, f, popup); - } else if self.app.mounted(&Id::OpenWithPopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::OpenWithPopup, f, popup); - } else if self.app.mounted(&Id::RenamePopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::RenamePopup, f, popup); - } else if self.app.mounted(&Id::SaveAsPopup) { - let popup = Popup(Size::Percentage(40), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::SaveAsPopup, f, popup); - } else if self.app.mounted(&Id::SymlinkPopup) { - let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::SymlinkPopup, f, popup); - } else if self.app.mounted(&Id::FileInfoPopup) { - let popup = Popup(Size::Percentage(80), Size::Percentage(50)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::FileInfoPopup, f, popup); - } else if self.app.mounted(&Id::ProgressBarPartial) { - let popup = Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - let popup_chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(50), // Full - Constraint::Percentage(50), // Partial - ] - .as_ref(), - ) - .split(popup); - self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]); - self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]); - } else if self.app.mounted(&Id::DeletePopup) { - let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::DeletePopup, f, popup); - } else if self.app.mounted(&Id::ReplacePopup) { - let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::ReplacePopup, f, popup); - } else if self.app.mounted(&Id::DisconnectPopup) { - let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::DisconnectPopup, f, popup); - } else if self.app.mounted(&Id::QuitPopup) { - let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::QuitPopup, f, popup); - } else if self.app.mounted(&Id::WatchedPathsList) { - let popup = Popup(Size::Percentage(60), Size::Percentage(50)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::WatchedPathsList, f, popup); - } else if self.app.mounted(&Id::WatcherPopup) { - let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::WatcherPopup, f, popup); - } else if self.app.mounted(&Id::SortingPopup) { - let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::SortingPopup, f, popup); - } else if self.app.mounted(&Id::ErrorPopup) { - let popup = Popup( - Size::Percentage(50), - self.calc_popup_height(Id::ErrorPopup, f.area().width, f.area().height), - ) - .draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::ErrorPopup, f, popup); - } else if self.app.mounted(&Id::WaitPopup) { - let wait_popup_lines = self - .app - .query(&Id::WaitPopup, Attribute::Text) - .map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len())) - .unwrap_or_default() - .unwrap_or(1) as u16; - - let popup = - Popup(Size::Percentage(50), Size::Unit(2 + wait_popup_lines)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::WaitPopup, f, popup); - } else if self.app.mounted(&Id::SyncBrowsingMkdirPopup) { - let popup = Popup(Size::Percentage(60), Size::Unit(3)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::SyncBrowsingMkdirPopup, f, popup); - } else if self.app.mounted(&Id::KeybindingsPopup) { - let popup = Popup(Size::Percentage(50), Size::Percentage(80)).draw_in(f.area()); - f.render_widget(Clear, popup); - // make popup - self.app.view(&Id::KeybindingsPopup, f, popup); - } - }); - // Re-give context - self.context = Some(context); - } - - // -- partials - - /// Mount info box - pub(super) fn mount_info>(&mut self, text: S) { - // Mount - let info_color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::ErrorPopup, - Box::new(components::ErrorPopup::new(text, info_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ErrorPopup).is_ok()); - } - - /// Mount error box - pub(super) fn mount_error>(&mut self, text: S) { - // Mount - let error_color = self.theme().misc_error_dialog; - assert!( - self.app - .remount( - Id::ErrorPopup, - Box::new(components::ErrorPopup::new(text, error_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ErrorPopup).is_ok()); - } - - /// Umount error message - pub(super) fn umount_error(&mut self) { - let _ = self.app.umount(&Id::ErrorPopup); - } - - pub(super) fn mount_fatal>(&mut self, text: S) { - self.umount_wait(); - // Mount - let error_color = self.theme().misc_error_dialog; - assert!( - self.app - .remount( - Id::FatalPopup, - Box::new(components::FatalPopup::new(text, error_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::FatalPopup).is_ok()); - } - - /// Umount fatal error message - pub(super) fn umount_fatal(&mut self) { - let _ = self.app.umount(&Id::FatalPopup); - } - - pub(super) fn mount_wait>(&mut self, text: S) { - let color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::WaitPopup, - Box::new(components::WaitPopup::new(text, color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::WaitPopup).is_ok()); - } - - pub(super) fn mount_walkdir_wait(&mut self) { - let color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::WaitPopup, - Box::new(components::WalkdirWaitPopup::new( - "Scanning current directory…", - color - )), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::WaitPopup).is_ok()); - - self.view(); - } - - pub(super) fn update_walkdir_entries(&mut self, entries: usize) { - let text = format!("Scanning current directory… ({entries} items found)",); - let _ = self.app.attr( - &Id::WaitPopup, - Attribute::Text, - AttrValue::Payload(PropPayload::Vec(vec![ - PropValue::TextSpan(TextSpan::from(text)), - PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")), - ])), - ); - - self.view(); - } - - pub(super) fn mount_blocking_wait>(&mut self, text: S) { - self.mount_wait(text); - self.view(); - } - - pub(super) fn umount_wait(&mut self) { - let _ = self.app.umount(&Id::WaitPopup); - } - - /// Mount quit popup - pub(super) fn mount_quit(&mut self) { - // Protocol - let quit_color = self.theme().misc_quit_dialog; - assert!( - self.app - .remount( - Id::QuitPopup, - Box::new(components::QuitPopup::new(quit_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::QuitPopup).is_ok()); - } - - /// Umount quit popup - pub(super) fn umount_quit(&mut self) { - let _ = self.app.umount(&Id::QuitPopup); - } - - /// Mount disconnect popup - pub(super) fn mount_disconnect(&mut self) { - // Protocol - let quit_color = self.theme().misc_quit_dialog; - assert!( - self.app - .remount( - Id::DisconnectPopup, - Box::new(components::DisconnectPopup::new(quit_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::DisconnectPopup).is_ok()); - } - - /// Umount disconnect popup - pub(super) fn umount_disconnect(&mut self) { - let _ = self.app.umount(&Id::DisconnectPopup); - } - - pub(super) fn mount_chmod(&mut self, mode: UnixPex, title: String) { - // Mount - let color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::ChmodPopup, - Box::new(components::ChmodPopup::new(mode, color, title)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ChmodPopup).is_ok()); - } - - pub(super) fn umount_chmod(&mut self) { - let _ = self.app.umount(&Id::ChmodPopup); - } - - pub(super) fn umount_filter(&mut self) { - let _ = self.app.umount(&Id::FilterPopup); - } - - pub(super) fn mount_filter(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::FilterPopup, - Box::new(components::FilterPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::FilterPopup).is_ok()); - } - - pub(super) fn mount_copy(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::CopyPopup, - Box::new(components::CopyPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::CopyPopup).is_ok()); - } - - pub(super) fn umount_copy(&mut self) { - let _ = self.app.umount(&Id::CopyPopup); - } - - pub(super) fn mount_exec(&mut self) { - let tab = self.browser.tab(); - let id = match tab { - FileExplorerTab::HostBridge => Id::TerminalHostBridge, - FileExplorerTab::Remote => Id::TerminalRemote, - _ => panic!("Cannot mount terminal on this tab"), - }; - - let border = match tab { - FileExplorerTab::HostBridge => self.theme().transfer_local_explorer_highlighted, - FileExplorerTab::Remote => self.theme().transfer_remote_explorer_highlighted, - _ => panic!("Cannot mount terminal on this tab"), - }; - - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - id.clone(), - Box::new( - components::Terminal::default() - .foreground(input_color) - .prompt(self.terminal_prompt()) - .title(format!("Terminal - {}", self.get_tab_hostname())) - .border_color(border) - ), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&id).is_ok()); - } - - /// Update the terminal prompt based on the current directory - pub(super) fn update_terminal_prompt(&mut self) { - let prompt = self.terminal_prompt(); - let id = match self.browser.tab() { - FileExplorerTab::HostBridge => Id::TerminalHostBridge, - FileExplorerTab::Remote => Id::TerminalRemote, - _ => panic!("Cannot update terminal prompt on this tab"), - }; - let _ = self - .app - .attr(&id, Attribute::Content, AttrValue::String(prompt)); - } - - /// Print output to terminal - pub(super) fn print_terminal(&mut self, text: String) { - // get id - let focus = self.app.focus().unwrap().clone(); - - // replace all \n with \r\n - let mut text = text.replace('\n', "\r\n"); - if !text.ends_with("\r\n") && !text.is_empty() { - text.push_str("\r\n"); - } - let _ = self - .app - .attr(&focus, Attribute::Text, AttrValue::String(text)); - } - - pub(super) fn umount_exec(&mut self) { - let focus = self.app.focus().unwrap().clone(); - let _ = self.app.umount(&focus); - } - - pub(super) fn mount_find(&mut self, msg: impl ToString, fuzzy_search: bool) { - // Get color - let (bg, fg, hg) = match self.browser.tab() { - FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => ( - self.theme().transfer_local_explorer_background, - self.theme().transfer_local_explorer_foreground, - self.theme().transfer_local_explorer_highlighted, - ), - FileExplorerTab::Remote | FileExplorerTab::FindRemote => ( - self.theme().transfer_remote_explorer_background, - self.theme().transfer_remote_explorer_foreground, - self.theme().transfer_remote_explorer_highlighted, - ), - }; - - // Mount component - assert!( - self.app - .remount( - Id::ExplorerFind, - if fuzzy_search { - Box::new(components::ExplorerFuzzy::new( - msg.to_string(), - &[], - bg, - fg, - hg, - )) - } else { - Box::new(components::ExplorerFind::new( - msg.to_string(), - &[], - bg, - fg, - hg, - )) - }, - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ExplorerFind).is_ok()); - } - - pub(super) fn umount_find(&mut self) { - let _ = self.app.umount(&Id::ExplorerFind); - } - - pub(super) fn mount_goto(&mut self) { - // get files - let files = self - .browser - .explorer() - .iter_files() - .filter(|f| f.is_dir() || f.is_symlink()) - .map(|f| f.path().to_string_lossy().to_string()) - .collect::>(); - - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::GotoPopup, - Box::new(components::GotoPopup::new(input_color, files)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::GotoPopup).is_ok()); - } - - pub(super) fn update_goto(&mut self, files: Vec) { - let payload = files - .into_iter() - .map(PropValue::Str) - .collect::>(); - - let _ = self.app.attr( - &Id::GotoPopup, - Attribute::Custom(ATTR_FILES), - AttrValue::Payload(PropPayload::Vec(payload)), - ); - } - - pub(super) fn umount_goto(&mut self) { - let _ = self.app.umount(&Id::GotoPopup); - } - - pub(super) fn mount_mkdir(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::MkdirPopup, - Box::new(components::MkdirPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::MkdirPopup).is_ok()); - } - - pub(super) fn umount_mkdir(&mut self) { - let _ = self.app.umount(&Id::MkdirPopup); - } - - pub(super) fn mount_newfile(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::NewfilePopup, - Box::new(components::NewfilePopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::NewfilePopup).is_ok()); - } - - pub(super) fn umount_newfile(&mut self) { - let _ = self.app.umount(&Id::NewfilePopup); - } - - pub(super) fn mount_openwith(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::OpenWithPopup, - Box::new(components::OpenWithPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::OpenWithPopup).is_ok()); - } - - pub(super) fn umount_openwith(&mut self) { - let _ = self.app.umount(&Id::OpenWithPopup); - } - - pub(super) fn mount_rename(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::RenamePopup, - Box::new(components::RenamePopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::RenamePopup).is_ok()); - } - - pub(super) fn umount_rename(&mut self) { - let _ = self.app.umount(&Id::RenamePopup); - } - - pub(super) fn mount_saveas(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::SaveAsPopup, - Box::new(components::SaveAsPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::SaveAsPopup).is_ok()); - } - - pub(super) fn umount_saveas(&mut self) { - let _ = self.app.umount(&Id::SaveAsPopup); - } - - pub(super) fn mount_progress_bar(&mut self, root_name: String) { - let prog_color_full = self.theme().transfer_progress_bar_full; - let prog_color_partial = self.theme().transfer_progress_bar_partial; - assert!( - self.app - .remount( - Id::ProgressBarFull, - Box::new(components::ProgressBarFull::new( - 0.0, - "", - &root_name, - prog_color_full - )), - vec![], - ) - .is_ok() - ); - assert!( - self.app - .remount( - Id::ProgressBarPartial, - Box::new(components::ProgressBarPartial::new( - 0.0, - "", - "Please wait", - prog_color_partial - )), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ProgressBarPartial).is_ok()); - } - - pub(super) fn umount_progress_bar(&mut self) { - let _ = self.app.umount(&Id::ProgressBarPartial); - let _ = self.app.umount(&Id::ProgressBarFull); - } - - pub(super) fn mount_file_sorting(&mut self) { - let sorting_color = self.theme().transfer_status_sorting; - let sorting: FileSorting = match self.browser.tab() { - FileExplorerTab::HostBridge => self.host_bridge().get_file_sorting(), - FileExplorerTab::Remote => self.remote().get_file_sorting(), - _ => return, - }; - assert!( - self.app - .remount( - Id::SortingPopup, - Box::new(components::SortingPopup::new(sorting, sorting_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::SortingPopup).is_ok()); - } - - pub(super) fn umount_file_sorting(&mut self) { - let _ = self.app.umount(&Id::SortingPopup); - } - - pub(super) fn mount_radio_delete(&mut self) { - let warn_color = self.theme().misc_warn_dialog; - assert!( - self.app - .remount( - Id::DeletePopup, - Box::new(components::DeletePopup::new(warn_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::DeletePopup).is_ok()); - } - - pub(super) fn umount_radio_delete(&mut self) { - let _ = self.app.umount(&Id::DeletePopup); - } - - pub(super) fn mount_radio_watch(&mut self, watch: bool, local: &str, remote: &str) { - let info_color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::WatcherPopup, - Box::new(components::WatcherPopup::new( - watch, local, remote, info_color - )), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::WatcherPopup).is_ok()); - } - - pub(super) fn umount_radio_watcher(&mut self) { - let _ = self.app.umount(&Id::WatcherPopup); - } - - pub(super) fn mount_watched_paths_list(&mut self, paths: &[std::path::PathBuf]) { - let info_color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::WatchedPathsList, - Box::new(components::WatchedPathsList::new(paths, info_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::WatchedPathsList).is_ok()); - } - - pub(super) fn umount_watched_paths_list(&mut self) { - let _ = self.app.umount(&Id::WatchedPathsList); - } - - pub(super) fn mount_radio_replace(&mut self, file_name: &str) { - let warn_color = self.theme().misc_warn_dialog; - assert!( - self.app - .remount( - Id::ReplacePopup, - Box::new(components::ReplacePopup::new(Some(file_name), warn_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::ReplacePopup).is_ok()); - } - - pub(super) fn umount_radio_replace(&mut self) { - let _ = self.app.umount(&Id::ReplacePopup); - } - - pub(super) fn mount_file_info(&mut self, file: &File) { - assert!( - self.app - .remount( - Id::FileInfoPopup, - Box::new(components::FileInfoPopup::new(file)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::FileInfoPopup).is_ok()); - } - - pub(super) fn umount_file_info(&mut self) { - let _ = self.app.umount(&Id::FileInfoPopup); - } - - pub(super) fn refresh_host_bridge_transfer_queue(&mut self) { - let enqueued = self - .host_bridge() - .enqueued() - .iter() - .map(|(src, dest)| (src.clone(), dest.clone())) - .collect::>(); - let log_panel = self.theme().transfer_log_window; - - assert!( - self.app - .remount( - Id::TransferQueueHostBridge, - Box::new(components::SelectedFilesList::new( - &enqueued, - MarkQueue::Local, - log_panel, - "Host Bridge selected files", - )), - vec![] - ) - .is_ok() - ); - } - - pub(super) fn refresh_remote_transfer_queue(&mut self) { - let enqueued = self - .remote() - .enqueued() - .iter() - .map(|(src, dest)| (src.clone(), dest.clone())) - .collect::>(); - let log_panel = self.theme().transfer_log_window; - - assert!( - self.app - .remount( - Id::TransferQueueRemote, - Box::new(components::SelectedFilesList::new( - &enqueued, - MarkQueue::Remote, - log_panel, - "Remote transfer selected files", - )), - vec![] - ) - .is_ok() - ); - } - - pub(super) fn refresh_local_status_bar(&mut self) { - let sorting_color = self.theme().transfer_status_sorting; - let hidden_color = self.theme().transfer_status_hidden; - assert!( - self.app - .remount( - Id::StatusBarHostBridge, - Box::new(components::StatusBarLocal::new( - &self.browser, - sorting_color, - hidden_color - )), - vec![], - ) - .is_ok() - ); - } - - pub(super) fn refresh_remote_status_bar(&mut self) { - let sorting_color = self.theme().transfer_status_sorting; - let hidden_color = self.theme().transfer_status_hidden; - let sync_color = self.theme().transfer_status_sync_browsing; - assert!( - self.app - .remount( - Id::StatusBarRemote, - Box::new(components::StatusBarRemote::new( - &self.browser, - sorting_color, - hidden_color, - sync_color - )), - vec![], - ) - .is_ok() - ); - } - - pub(super) fn mount_symlink(&mut self) { - let input_color = self.theme().misc_input_dialog; - assert!( - self.app - .remount( - Id::SymlinkPopup, - Box::new(components::SymlinkPopup::new(input_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::SymlinkPopup).is_ok()); - } - - pub(super) fn umount_symlink(&mut self) { - let _ = self.app.umount(&Id::SymlinkPopup); - } - - pub(super) fn mount_sync_browsing_mkdir_popup(&mut self, dir_name: &str) { - let color = self.theme().misc_info_dialog; - assert!( - self.app - .remount( - Id::SyncBrowsingMkdirPopup, - Box::new(components::SyncBrowsingMkdirPopup::new(color, dir_name,)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::SyncBrowsingMkdirPopup).is_ok()); - } - - pub(super) fn umount_sync_browsing_mkdir_popup(&mut self) { - let _ = self.app.umount(&Id::SyncBrowsingMkdirPopup); - } - - /// Mount help - pub(super) fn mount_help(&mut self) { - let key_color = self.theme().misc_keys; - assert!( - self.app - .remount( - Id::KeybindingsPopup, - Box::new(components::KeybindingsPopup::new(key_color)), - vec![], - ) - .is_ok() - ); - assert!(self.app.active(&Id::KeybindingsPopup).is_ok()); - } - - pub(super) fn umount_help(&mut self) { - let _ = self.app.umount(&Id::KeybindingsPopup); - } - - // -- dynamic size - - /// Given the id of the component to display and the width and height of the total area, - /// returns the height in percentage to the entire area height, that the popup should have - fn calc_popup_height(&self, id: Id, width: u16, height: u16) -> Size { - // Get current text width - let text_width = self - .app - .query(&id, tuirealm::Attribute::Text) - .ok() - .flatten() - .map(|x| { - if x.as_payload().is_none() { - return 0; - } - - x.unwrap_payload() - .unwrap_vec() - .into_iter() - .map(|x| x.unwrap_text_span().content) - .collect::>() - .join("") - .width() as u16 - }) - .unwrap_or(0); - // Calc real width of a row in the popup - let row_width = (width / 2).saturating_sub(2); - // Calc row height in percentage (1 : height = x : 100) - let row_height_p = (100.0 / (height as f64)).ceil() as u16; - // Get amount of required rows NOTE: + 2 because of margins - let display_rows = ((text_width as f64) / (row_width as f64)).ceil() as u16 + 2; - // Return height (row_height_p * display_rows) - Size::Percentage(display_rows * row_height_p) - } - - // -- global listener - - fn mount_global_listener(&mut self) { - assert!( - self.app - .mount( - Id::GlobalListener, - Box::::default(), - vec![ - Sub::new( - SubEventClause::Keyboard(KeyEvent { - code: Key::Esc, - modifiers: KeyModifiers::NONE, - }), - Self::no_popup_mounted_clause(), - ), - Sub::new( - SubEventClause::Keyboard(KeyEvent { - code: Key::Char('h'), - modifiers: KeyModifiers::NONE, - }), - Self::no_popup_mounted_clause(), - ), - Sub::new( - SubEventClause::Keyboard(KeyEvent { - code: Key::Function(1), - modifiers: KeyModifiers::NONE, - }), - Self::no_popup_mounted_clause(), - ), - Sub::new( - SubEventClause::Keyboard(KeyEvent { - code: Key::Function(10), - modifiers: KeyModifiers::NONE, - }), - Self::no_popup_mounted_clause(), - ), - Sub::new( - SubEventClause::Keyboard(KeyEvent { - code: Key::Char('q'), - modifiers: KeyModifiers::NONE, - }), - Self::no_popup_mounted_clause(), - ), - Sub::new(SubEventClause::WindowResize, SubClause::Always) - ] - ) - .is_ok() - ); - } - - /// Returns a sub clause which requires that no popup is mounted in order to be satisfied - fn no_popup_mounted_clause() -> SubClause { - tuirealm::subclause_and_not!( - Id::CopyPopup, - Id::DeletePopup, - Id::DisconnectPopup, - Id::ErrorPopup, - Id::TerminalHostBridge, - Id::TerminalRemote, - Id::FatalPopup, - Id::FileInfoPopup, - Id::GotoPopup, - Id::KeybindingsPopup, - Id::MkdirPopup, - Id::NewfilePopup, - Id::OpenWithPopup, - Id::ProgressBarFull, - Id::ProgressBarPartial, - Id::ExplorerFind, - Id::QuitPopup, - Id::RenamePopup, - Id::ReplacePopup, - Id::SaveAsPopup, - Id::SortingPopup, - Id::SyncBrowsingMkdirPopup, - Id::SymlinkPopup, - Id::WatcherPopup, - Id::WatchedPathsList, - Id::ChmodPopup, - Id::WaitPopup, - Id::FilterPopup - ) - } -} +mod layout; +mod popups; +mod status; diff --git a/src/ui/activities/filetransfer/view/layout.rs b/src/ui/activities/filetransfer/view/layout.rs new file mode 100644 index 0000000..032f278 --- /dev/null +++ b/src/ui/activities/filetransfer/view/layout.rs @@ -0,0 +1,388 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; +use tuirealm::ratatui::widgets::Clear; +use tuirealm::{Attribute, Sub, SubClause, SubEventClause}; +use unicode_width::UnicodeWidthStr; + +use crate::ui::activities::filetransfer::browser::FoundExplorerTab; +use crate::ui::activities::filetransfer::{ + Context, FileTransferActivity, Id, components, ui_result, +}; +use crate::utils::ui::{Popup, Size}; + +impl FileTransferActivity { + // -- init + + /// Initialize file transfer activity's view + pub(in crate::ui::activities::filetransfer) fn init(&mut self) { + // Mount local file explorer + let local_explorer_background = self.theme().transfer_local_explorer_background; + let local_explorer_foreground = self.theme().transfer_local_explorer_foreground; + let local_explorer_highlighted = self.theme().transfer_local_explorer_highlighted; + let remote_explorer_background = self.theme().transfer_remote_explorer_background; + let remote_explorer_foreground = self.theme().transfer_remote_explorer_foreground; + let remote_explorer_highlighted = self.theme().transfer_remote_explorer_highlighted; + let key_color = self.theme().misc_keys; + let log_panel = self.theme().transfer_log_window; + let log_background = self.theme().transfer_log_background; + ui_result(self.app.mount( + Id::FooterBar, + Box::new(components::FooterBar::new(key_color)), + vec![], + )); + ui_result(self.app.mount( + Id::ExplorerHostBridge, + Box::new(components::ExplorerLocal::new( + "", + &[], + local_explorer_background, + local_explorer_foreground, + local_explorer_highlighted, + )), + vec![], + )); + ui_result(self.app.mount( + Id::ExplorerRemote, + Box::new(components::ExplorerRemote::new( + "", + &[], + remote_explorer_background, + remote_explorer_foreground, + remote_explorer_highlighted, + )), + vec![], + )); + ui_result(self.app.mount( + Id::Log, + Box::new(components::Log::new(vec![], log_panel, log_background)), + vec![], + )); + self.refresh_host_bridge_transfer_queue(); + self.refresh_remote_transfer_queue(); + // Load status bar + self.refresh_local_status_bar(); + self.refresh_remote_status_bar(); + // Update components + self.update_host_bridge_filelist(); + // self.update_remote_filelist(); + // Global listener + self.mount_global_listener(); + // Give focus to local explorer + ui_result(self.app.active(&Id::ExplorerHostBridge)); + } + + // -- view + + /// View gui + pub(in crate::ui::activities::filetransfer) fn view(&mut self) { + self.redraw = false; + let mut context: Context = self.context.take().unwrap(); + let _ = context.terminal.raw_mut().draw(|f| { + // Prepare chunks + let body = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Min(7), // Body + Constraint::Length(1), // Footer + ] + .as_ref(), + ) + .split(f.area()); + // main chunks + let main_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [ + Constraint::Percentage(70), // Explorer + Constraint::Percentage(30), // Log + ] + .as_ref(), + ) + .split(body[0]); + // Create explorer chunks + let tabs_chunks = Layout::default() + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .direction(Direction::Horizontal) + .split(main_chunks[0]); + // Create log box chunks + let bottom_chunks = Layout::default() + .constraints([Constraint::Length(1), Constraint::Length(10)].as_ref()) + .direction(Direction::Vertical) + .split(main_chunks[1]); + // Create status bar chunks + let status_bar_chunks = Layout::default() + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .direction(Direction::Horizontal) + .horizontal_margin(1) + .split(bottom_chunks[0]); + let bottom_components = Layout::default() + .constraints( + [ + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(50), + ] + .as_ref(), + ) + .direction(Direction::Horizontal) + .split(bottom_chunks[1]); + // Draw footer + self.app.view(&Id::FooterBar, f, body[1]); + // Draw explorers + // @! Local explorer (Find or default) + if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Local)) { + self.app.view(&Id::ExplorerFind, f, tabs_chunks[0]); + } else if self.browser.is_terminal_open_host_bridge() { + self.app.view(&Id::TerminalHostBridge, f, tabs_chunks[0]); + } else { + self.app.view(&Id::ExplorerHostBridge, f, tabs_chunks[0]); + } + // @! Remote explorer (Find or default) + if matches!(self.browser.found_tab(), Some(FoundExplorerTab::Remote)) { + self.app.view(&Id::ExplorerFind, f, tabs_chunks[1]); + } else if self.browser.is_terminal_open_remote() { + self.app.view(&Id::TerminalRemote, f, tabs_chunks[1]); + } else { + self.app.view(&Id::ExplorerRemote, f, tabs_chunks[1]); + } + // draw transfer queues + self.app + .view(&Id::TransferQueueHostBridge, f, bottom_components[0]); + self.app + .view(&Id::TransferQueueRemote, f, bottom_components[1]); + // Draw log box + self.app.view(&Id::Log, f, bottom_components[2]); + // Draw status bar + self.app + .view(&Id::StatusBarHostBridge, f, status_bar_chunks[0]); + self.app.view(&Id::StatusBarRemote, f, status_bar_chunks[1]); + // @! Draw popups — first mounted popup in priority order wins + let popup_priority = [ + Id::FatalPopup, + Id::CopyPopup, + Id::ChmodPopup, + Id::FilterPopup, + Id::GotoPopup, + Id::MkdirPopup, + Id::NewfilePopup, + Id::OpenWithPopup, + Id::RenamePopup, + Id::SaveAsPopup, + Id::SymlinkPopup, + Id::FileInfoPopup, + Id::ProgressBarPartial, + Id::DeletePopup, + Id::ReplacePopup, + Id::DisconnectPopup, + Id::QuitPopup, + Id::WatchedPathsList, + Id::WatcherPopup, + Id::SortingPopup, + Id::ErrorPopup, + Id::WaitPopup, + Id::SyncBrowsingMkdirPopup, + Id::KeybindingsPopup, + ]; + if let Some(popup_id) = popup_priority.iter().find(|id| self.app.mounted(id)) { + match popup_id { + // Dynamic-height popups (text wrapping) + Id::FatalPopup | Id::ErrorPopup => { + let popup = Popup( + Size::Percentage(50), + self.calc_popup_height( + popup_id.clone(), + f.area().width, + f.area().height, + ), + ) + .draw_in(f.area()); + f.render_widget(Clear, popup); + self.app.view(popup_id, f, popup); + } + // Dual-component progress bar + Id::ProgressBarPartial => { + let popup = + Popup(Size::Percentage(50), Size::Percentage(20)).draw_in(f.area()); + f.render_widget(Clear, popup); + let popup_chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [Constraint::Percentage(50), Constraint::Percentage(50)].as_ref(), + ) + .split(popup); + self.app.view(&Id::ProgressBarFull, f, popup_chunks[0]); + self.app.view(&Id::ProgressBarPartial, f, popup_chunks[1]); + } + // Wait popup with dynamic line count + Id::WaitPopup => { + let lines = self + .app + .query(&Id::WaitPopup, Attribute::Text) + .map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len())) + .unwrap_or_default() + .unwrap_or(1) as u16; + let popup = + Popup(Size::Percentage(50), Size::Unit(2 + lines)).draw_in(f.area()); + f.render_widget(Clear, popup); + self.app.view(&Id::WaitPopup, f, popup); + } + // Standard fixed-size popups + id => { + let (w, h) = Self::popup_dimensions(id); + let popup = Popup(w, h).draw_in(f.area()); + f.render_widget(Clear, popup); + self.app.view(id, f, popup); + } + } + } + }); + // Re-give context + self.context = Some(context); + } + + // -- popup dimensions + + /// Returns the fixed (width, height) for a standard popup. + fn popup_dimensions(id: &Id) -> (Size, Size) { + match id { + Id::CopyPopup + | Id::GotoPopup + | Id::MkdirPopup + | Id::NewfilePopup + | Id::OpenWithPopup + | Id::RenamePopup + | Id::SaveAsPopup => (Size::Percentage(40), Size::Unit(3)), + Id::ChmodPopup => (Size::Percentage(50), Size::Unit(12)), + Id::FilterPopup | Id::SymlinkPopup | Id::SortingPopup | Id::ReplacePopup => { + (Size::Percentage(50), Size::Unit(3)) + } + Id::FileInfoPopup => (Size::Percentage(80), Size::Percentage(50)), + Id::DeletePopup | Id::DisconnectPopup | Id::QuitPopup => { + (Size::Percentage(30), Size::Unit(3)) + } + Id::WatchedPathsList => (Size::Percentage(60), Size::Percentage(50)), + Id::WatcherPopup | Id::SyncBrowsingMkdirPopup => (Size::Percentage(60), Size::Unit(3)), + Id::KeybindingsPopup => (Size::Percentage(50), Size::Percentage(80)), + _ => (Size::Percentage(50), Size::Unit(3)), + } + } + + /// Given the id of the component to display and the width and height of the total area, + /// returns the height in percentage to the entire area height, that the popup should have + fn calc_popup_height(&self, id: Id, width: u16, height: u16) -> Size { + // Get current text width + let text_width = self + .app + .query(&id, tuirealm::Attribute::Text) + .ok() + .flatten() + .map(|x| { + if x.as_payload().is_none() { + return 0; + } + + x.unwrap_payload() + .unwrap_vec() + .into_iter() + .map(|x| x.unwrap_text_span().content) + .collect::>() + .join("") + .width() as u16 + }) + .unwrap_or(0); + // Calc real width of a row in the popup + let row_width = (width / 2).saturating_sub(2); + // Calc row height in percentage (1 : height = x : 100) + let row_height_p = (100.0 / (height as f64)).ceil() as u16; + // Get amount of required rows NOTE: + 2 because of margins + let display_rows = ((text_width as f64) / (row_width as f64)).ceil() as u16 + 2; + // Return height (row_height_p * display_rows) + Size::Percentage(display_rows * row_height_p) + } + + // -- global listener + + fn mount_global_listener(&mut self) { + ui_result(self.app.mount( + Id::GlobalListener, + Box::::default(), + vec![ + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Esc, + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('h'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(1), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Function(10), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new( + SubEventClause::Keyboard(KeyEvent { + code: Key::Char('q'), + modifiers: KeyModifiers::NONE, + }), + Self::no_popup_mounted_clause(), + ), + Sub::new(SubEventClause::WindowResize, SubClause::Always), + ], + )); + } + + /// Returns a sub clause which requires that no popup is mounted in order to be satisfied + fn no_popup_mounted_clause() -> SubClause { + tuirealm::subclause_and_not!( + Id::CopyPopup, + Id::DeletePopup, + Id::DisconnectPopup, + Id::ErrorPopup, + Id::TerminalHostBridge, + Id::TerminalRemote, + Id::FatalPopup, + Id::FileInfoPopup, + Id::GotoPopup, + Id::KeybindingsPopup, + Id::MkdirPopup, + Id::NewfilePopup, + Id::OpenWithPopup, + Id::ProgressBarFull, + Id::ProgressBarPartial, + Id::ExplorerFind, + Id::QuitPopup, + Id::RenamePopup, + Id::ReplacePopup, + Id::SaveAsPopup, + Id::SortingPopup, + Id::SyncBrowsingMkdirPopup, + Id::SymlinkPopup, + Id::WatcherPopup, + Id::WatchedPathsList, + Id::ChmodPopup, + Id::WaitPopup, + Id::FilterPopup + ) + } +} diff --git a/src/ui/activities/filetransfer/view/popups.rs b/src/ui/activities/filetransfer/view/popups.rs new file mode 100644 index 0000000..35805ca --- /dev/null +++ b/src/ui/activities/filetransfer/view/popups.rs @@ -0,0 +1,602 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use remotefs::fs::{File, UnixPex}; +use tuirealm::props::{PropPayload, PropValue, TextSpan}; +use tuirealm::{AttrValue, Attribute}; + +use crate::explorer::FileSorting; +use crate::ui::activities::filetransfer::browser::FileExplorerTab; +use crate::ui::activities::filetransfer::components::ATTR_FILES; +use crate::ui::activities::filetransfer::{FileTransferActivity, Id, components, ui_result}; + +impl FileTransferActivity { + // -- partials + + /// Mount info box + pub(in crate::ui::activities::filetransfer) fn mount_info>(&mut self, text: S) { + // Mount + let info_color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, info_color)), + vec![], + )); + ui_result(self.app.active(&Id::ErrorPopup)); + } + + /// Mount error box + pub(in crate::ui::activities::filetransfer) fn mount_error>(&mut self, text: S) { + // Mount + let error_color = self.theme().misc_error_dialog; + ui_result(self.app.remount( + Id::ErrorPopup, + Box::new(components::ErrorPopup::new(text, error_color)), + vec![], + )); + ui_result(self.app.active(&Id::ErrorPopup)); + } + + /// Umount error message + pub(in crate::ui::activities::filetransfer) fn umount_error(&mut self) { + let _ = self.app.umount(&Id::ErrorPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_fatal>(&mut self, text: S) { + self.umount_wait(); + // Mount + let error_color = self.theme().misc_error_dialog; + ui_result(self.app.remount( + Id::FatalPopup, + Box::new(components::FatalPopup::new(text, error_color)), + vec![], + )); + ui_result(self.app.active(&Id::FatalPopup)); + } + + /// Umount fatal error message + pub(in crate::ui::activities::filetransfer) fn umount_fatal(&mut self) { + let _ = self.app.umount(&Id::FatalPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_wait>(&mut self, text: S) { + let color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::WaitPopup, + Box::new(components::WaitPopup::new(text, color)), + vec![], + )); + ui_result(self.app.active(&Id::WaitPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn mount_walkdir_wait(&mut self) { + let color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::WaitPopup, + Box::new(components::WalkdirWaitPopup::new( + "Scanning current directory…", + color, + )), + vec![], + )); + ui_result(self.app.active(&Id::WaitPopup)); + + self.view(); + } + + pub(in crate::ui::activities::filetransfer) fn update_walkdir_entries( + &mut self, + entries: usize, + ) { + let text = format!("Scanning current directory… ({entries} items found)",); + let _ = self.app.attr( + &Id::WaitPopup, + Attribute::Text, + AttrValue::Payload(PropPayload::Vec(vec![ + PropValue::TextSpan(TextSpan::from(text)), + PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")), + ])), + ); + + self.view(); + } + + pub(in crate::ui::activities::filetransfer) fn mount_blocking_wait>( + &mut self, + text: S, + ) { + self.mount_wait(text); + self.view(); + } + + pub(in crate::ui::activities::filetransfer) fn umount_wait(&mut self) { + let _ = self.app.umount(&Id::WaitPopup); + } + + /// Mount quit popup + pub(in crate::ui::activities::filetransfer) fn mount_quit(&mut self) { + // Protocol + let quit_color = self.theme().misc_quit_dialog; + ui_result(self.app.remount( + Id::QuitPopup, + Box::new(components::QuitPopup::new(quit_color)), + vec![], + )); + ui_result(self.app.active(&Id::QuitPopup)); + } + + /// Umount quit popup + pub(in crate::ui::activities::filetransfer) fn umount_quit(&mut self) { + let _ = self.app.umount(&Id::QuitPopup); + } + + /// Mount disconnect popup + pub(in crate::ui::activities::filetransfer) fn mount_disconnect(&mut self) { + // Protocol + let quit_color = self.theme().misc_quit_dialog; + ui_result(self.app.remount( + Id::DisconnectPopup, + Box::new(components::DisconnectPopup::new(quit_color)), + vec![], + )); + ui_result(self.app.active(&Id::DisconnectPopup)); + } + + /// Umount disconnect popup + pub(in crate::ui::activities::filetransfer) fn umount_disconnect(&mut self) { + let _ = self.app.umount(&Id::DisconnectPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_chmod( + &mut self, + mode: UnixPex, + title: String, + ) { + // Mount + let color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::ChmodPopup, + Box::new(components::ChmodPopup::new(mode, color, title)), + vec![], + )); + ui_result(self.app.active(&Id::ChmodPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_chmod(&mut self) { + let _ = self.app.umount(&Id::ChmodPopup); + } + + pub(in crate::ui::activities::filetransfer) fn umount_filter(&mut self) { + let _ = self.app.umount(&Id::FilterPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_filter(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::FilterPopup, + Box::new(components::FilterPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::FilterPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn mount_copy(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::CopyPopup, + Box::new(components::CopyPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::CopyPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_copy(&mut self) { + let _ = self.app.umount(&Id::CopyPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_exec(&mut self) { + let tab = self.browser.tab(); + let id = match tab { + FileExplorerTab::HostBridge => Id::TerminalHostBridge, + FileExplorerTab::Remote => Id::TerminalRemote, + _ => { + error!("Cannot mount terminal on this tab"); + return; + } + }; + + let border = match tab { + FileExplorerTab::HostBridge => self.theme().transfer_local_explorer_highlighted, + FileExplorerTab::Remote => self.theme().transfer_remote_explorer_highlighted, + _ => { + error!("Cannot mount terminal on this tab"); + return; + } + }; + + let input_color = self.theme().misc_input_dialog; + ui_result( + self.app.remount( + id.clone(), + Box::new( + components::Terminal::default() + .foreground(input_color) + .prompt(self.terminal_prompt()) + .title(format!("Terminal - {}", self.get_tab_hostname())) + .border_color(border), + ), + vec![], + ), + ); + ui_result(self.app.active(&id)); + } + + /// Update the terminal prompt based on the current directory + pub(in crate::ui::activities::filetransfer) fn update_terminal_prompt(&mut self) { + let prompt = self.terminal_prompt(); + let id = match self.browser.tab() { + FileExplorerTab::HostBridge => Id::TerminalHostBridge, + FileExplorerTab::Remote => Id::TerminalRemote, + _ => { + error!("Cannot update terminal prompt on this tab"); + return; + } + }; + let _ = self + .app + .attr(&id, Attribute::Content, AttrValue::String(prompt)); + } + + /// Print output to terminal + pub(in crate::ui::activities::filetransfer) fn print_terminal(&mut self, text: String) { + // get id + let focus = self.app.focus().unwrap().clone(); + + // replace all \n with \r\n + let mut text = text.replace('\n', "\r\n"); + if !text.ends_with("\r\n") && !text.is_empty() { + text.push_str("\r\n"); + } + let _ = self + .app + .attr(&focus, Attribute::Text, AttrValue::String(text)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_exec(&mut self) { + let focus = self.app.focus().unwrap().clone(); + let _ = self.app.umount(&focus); + } + + pub(in crate::ui::activities::filetransfer) fn mount_find( + &mut self, + msg: impl ToString, + fuzzy_search: bool, + ) { + // Get color + let (bg, fg, hg) = match self.browser.tab() { + FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => ( + self.theme().transfer_local_explorer_background, + self.theme().transfer_local_explorer_foreground, + self.theme().transfer_local_explorer_highlighted, + ), + FileExplorerTab::Remote | FileExplorerTab::FindRemote => ( + self.theme().transfer_remote_explorer_background, + self.theme().transfer_remote_explorer_foreground, + self.theme().transfer_remote_explorer_highlighted, + ), + }; + + // Mount component + ui_result(self.app.remount( + Id::ExplorerFind, + if fuzzy_search { + Box::new(components::ExplorerFuzzy::new( + msg.to_string(), + &[], + bg, + fg, + hg, + )) + } else { + Box::new(components::ExplorerFind::new( + msg.to_string(), + &[], + bg, + fg, + hg, + )) + }, + vec![], + )); + ui_result(self.app.active(&Id::ExplorerFind)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_find(&mut self) { + let _ = self.app.umount(&Id::ExplorerFind); + } + + pub(in crate::ui::activities::filetransfer) fn mount_goto(&mut self) { + // get files + let files = self + .browser + .explorer() + .iter_files() + .filter(|f| f.is_dir() || f.is_symlink()) + .map(|f| f.path().to_string_lossy().to_string()) + .collect::>(); + + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::GotoPopup, + Box::new(components::GotoPopup::new(input_color, files)), + vec![], + )); + ui_result(self.app.active(&Id::GotoPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn update_goto(&mut self, files: Vec) { + let payload = files + .into_iter() + .map(PropValue::Str) + .collect::>(); + + let _ = self.app.attr( + &Id::GotoPopup, + Attribute::Custom(ATTR_FILES), + AttrValue::Payload(PropPayload::Vec(payload)), + ); + } + + pub(in crate::ui::activities::filetransfer) fn umount_goto(&mut self) { + let _ = self.app.umount(&Id::GotoPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_mkdir(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::MkdirPopup, + Box::new(components::MkdirPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::MkdirPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_mkdir(&mut self) { + let _ = self.app.umount(&Id::MkdirPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_newfile(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::NewfilePopup, + Box::new(components::NewfilePopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::NewfilePopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_newfile(&mut self) { + let _ = self.app.umount(&Id::NewfilePopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_openwith(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::OpenWithPopup, + Box::new(components::OpenWithPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::OpenWithPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_openwith(&mut self) { + let _ = self.app.umount(&Id::OpenWithPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_rename(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::RenamePopup, + Box::new(components::RenamePopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::RenamePopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_rename(&mut self) { + let _ = self.app.umount(&Id::RenamePopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_saveas(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::SaveAsPopup, + Box::new(components::SaveAsPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::SaveAsPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_saveas(&mut self) { + let _ = self.app.umount(&Id::SaveAsPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_progress_bar( + &mut self, + root_name: String, + ) { + let prog_color_full = self.theme().transfer_progress_bar_full; + let prog_color_partial = self.theme().transfer_progress_bar_partial; + ui_result(self.app.remount( + Id::ProgressBarFull, + Box::new(components::ProgressBarFull::new( + 0.0, + "", + &root_name, + prog_color_full, + )), + vec![], + )); + ui_result(self.app.remount( + Id::ProgressBarPartial, + Box::new(components::ProgressBarPartial::new( + 0.0, + "", + "Please wait", + prog_color_partial, + )), + vec![], + )); + ui_result(self.app.active(&Id::ProgressBarPartial)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_progress_bar(&mut self) { + let _ = self.app.umount(&Id::ProgressBarPartial); + let _ = self.app.umount(&Id::ProgressBarFull); + } + + pub(in crate::ui::activities::filetransfer) fn mount_file_sorting(&mut self) { + let sorting_color = self.theme().transfer_status_sorting; + let sorting: FileSorting = match self.browser.tab() { + FileExplorerTab::HostBridge => self.host_bridge().get_file_sorting(), + FileExplorerTab::Remote => self.remote().get_file_sorting(), + _ => return, + }; + ui_result(self.app.remount( + Id::SortingPopup, + Box::new(components::SortingPopup::new(sorting, sorting_color)), + vec![], + )); + ui_result(self.app.active(&Id::SortingPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_file_sorting(&mut self) { + let _ = self.app.umount(&Id::SortingPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_radio_delete(&mut self) { + let warn_color = self.theme().misc_warn_dialog; + ui_result(self.app.remount( + Id::DeletePopup, + Box::new(components::DeletePopup::new(warn_color)), + vec![], + )); + ui_result(self.app.active(&Id::DeletePopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_radio_delete(&mut self) { + let _ = self.app.umount(&Id::DeletePopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_radio_watch( + &mut self, + watch: bool, + local: &str, + remote: &str, + ) { + let info_color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::WatcherPopup, + Box::new(components::WatcherPopup::new( + watch, local, remote, info_color, + )), + vec![], + )); + ui_result(self.app.active(&Id::WatcherPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_radio_watcher(&mut self) { + let _ = self.app.umount(&Id::WatcherPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_watched_paths_list( + &mut self, + paths: &[std::path::PathBuf], + ) { + let info_color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::WatchedPathsList, + Box::new(components::WatchedPathsList::new(paths, info_color)), + vec![], + )); + ui_result(self.app.active(&Id::WatchedPathsList)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_watched_paths_list(&mut self) { + let _ = self.app.umount(&Id::WatchedPathsList); + } + + pub(in crate::ui::activities::filetransfer) fn mount_radio_replace(&mut self, file_name: &str) { + let warn_color = self.theme().misc_warn_dialog; + ui_result(self.app.remount( + Id::ReplacePopup, + Box::new(components::ReplacePopup::new(Some(file_name), warn_color)), + vec![], + )); + ui_result(self.app.active(&Id::ReplacePopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_radio_replace(&mut self) { + let _ = self.app.umount(&Id::ReplacePopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_file_info(&mut self, file: &File) { + ui_result(self.app.remount( + Id::FileInfoPopup, + Box::new(components::FileInfoPopup::new(file)), + vec![], + )); + ui_result(self.app.active(&Id::FileInfoPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_file_info(&mut self) { + let _ = self.app.umount(&Id::FileInfoPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_symlink(&mut self) { + let input_color = self.theme().misc_input_dialog; + ui_result(self.app.remount( + Id::SymlinkPopup, + Box::new(components::SymlinkPopup::new(input_color)), + vec![], + )); + ui_result(self.app.active(&Id::SymlinkPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_symlink(&mut self) { + let _ = self.app.umount(&Id::SymlinkPopup); + } + + pub(in crate::ui::activities::filetransfer) fn mount_sync_browsing_mkdir_popup( + &mut self, + dir_name: &str, + ) { + let color = self.theme().misc_info_dialog; + ui_result(self.app.remount( + Id::SyncBrowsingMkdirPopup, + Box::new(components::SyncBrowsingMkdirPopup::new(color, dir_name)), + vec![], + )); + ui_result(self.app.active(&Id::SyncBrowsingMkdirPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_sync_browsing_mkdir_popup(&mut self) { + let _ = self.app.umount(&Id::SyncBrowsingMkdirPopup); + } + + /// Mount help + pub(in crate::ui::activities::filetransfer) fn mount_help(&mut self) { + let key_color = self.theme().misc_keys; + ui_result(self.app.remount( + Id::KeybindingsPopup, + Box::new(components::KeybindingsPopup::new(key_color)), + vec![], + )); + ui_result(self.app.active(&Id::KeybindingsPopup)); + } + + pub(in crate::ui::activities::filetransfer) fn umount_help(&mut self) { + let _ = self.app.umount(&Id::KeybindingsPopup); + } +} diff --git a/src/ui/activities/filetransfer/view/status.rs b/src/ui/activities/filetransfer/view/status.rs new file mode 100644 index 0000000..74ee3c6 --- /dev/null +++ b/src/ui/activities/filetransfer/view/status.rs @@ -0,0 +1,81 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall + +use crate::ui::activities::filetransfer::{ + FileTransferActivity, Id, MarkQueue, components, ui_result, +}; + +impl FileTransferActivity { + pub(in crate::ui::activities::filetransfer) fn refresh_host_bridge_transfer_queue(&mut self) { + let enqueued = self + .host_bridge() + .enqueued() + .iter() + .map(|(src, dest)| (src.clone(), dest.clone())) + .collect::>(); + let log_panel = self.theme().transfer_log_window; + + ui_result(self.app.remount( + Id::TransferQueueHostBridge, + Box::new(components::SelectedFilesList::new( + &enqueued, + MarkQueue::Local, + log_panel, + "Host Bridge selected files", + )), + vec![], + )); + } + + pub(in crate::ui::activities::filetransfer) fn refresh_remote_transfer_queue(&mut self) { + let enqueued = self + .remote() + .enqueued() + .iter() + .map(|(src, dest)| (src.clone(), dest.clone())) + .collect::>(); + let log_panel = self.theme().transfer_log_window; + + ui_result(self.app.remount( + Id::TransferQueueRemote, + Box::new(components::SelectedFilesList::new( + &enqueued, + MarkQueue::Remote, + log_panel, + "Remote transfer selected files", + )), + vec![], + )); + } + + pub(in crate::ui::activities::filetransfer) fn refresh_local_status_bar(&mut self) { + let sorting_color = self.theme().transfer_status_sorting; + let hidden_color = self.theme().transfer_status_hidden; + ui_result(self.app.remount( + Id::StatusBarHostBridge, + Box::new(components::StatusBarLocal::new( + &self.browser, + sorting_color, + hidden_color, + )), + vec![], + )); + } + + pub(in crate::ui::activities::filetransfer) fn refresh_remote_status_bar(&mut self) { + let sorting_color = self.theme().transfer_status_sorting; + let hidden_color = self.theme().transfer_status_hidden; + let sync_color = self.theme().transfer_status_sync_browsing; + ui_result(self.app.remount( + Id::StatusBarRemote, + Box::new(components::StatusBarRemote::new( + &self.browser, + sorting_color, + hidden_color, + sync_color, + )), + vec![], + )); + } +} diff --git a/src/utils/parser.rs b/src/utils/parser.rs index a56fdc3..40bfe92 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -338,9 +338,9 @@ fn parse_kube_remote_opt(s: &str) -> Result { fn parse_smb_remote_opts(s: &str) -> Result { match REMOTE_SMB_OPT_REGEX.captures(s) { Some(groups) => { - let username: Option = match groups.get(1) { + let username = match groups.get(1) { Some(group) => Some(group.as_str().to_string()), - None => Some(whoami::username()), + None => whoami::username().ok(), }; let address = match groups.get(2) { Some(group) => group.as_str().to_string(), diff --git a/src/utils/random.rs b/src/utils/random.rs index b06bea7..05fc51e 100644 --- a/src/utils/random.rs +++ b/src/utils/random.rs @@ -5,7 +5,7 @@ // Ext use rand::distr::Alphanumeric; -use rand::{Rng, rng}; +use rand::{RngExt as _, rng}; /// Generate a random alphanumeric string with provided length pub fn random_alphanumeric_with_len(len: usize) -> String {

").bold().fg(key_color)) - .add_col(TextSpan::from(" Toggle bottom panel")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Quit termscp")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Rename file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Save file as")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Watch/unwatch file changes")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Go to parent directory")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with default application for file type", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Open file with specified application", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Execute shell command")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Toggle synchronized browsing", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Change file permissions")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Filter files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Delete selected file")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Select all files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Deselect all files")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Interrupt file transfer")) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from( - " Get total path size of selected files", - )) - .add_row() - .add_col(TextSpan::new("").bold().fg(key_color)) - .add_col(TextSpan::from(" Show watched paths")) - .build(), - ), - } - } -} - -impl Component for KeybindingsPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Esc | Key::Enter, - .. - }) => Some(Msg::Ui(UiMsg::CloseKeybindingsPopup)), - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => { - self.perform(Cmd::Move(Direction::Down)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - self.perform(Cmd::Move(Direction::Up)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::PageDown, - .. - }) => { - self.perform(Cmd::Scroll(Direction::Down)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::PageUp, .. - }) => { - self.perform(Cmd::Scroll(Direction::Up)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct MkdirPopup { - component: Input, -} - -impl MkdirPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "New directory name", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("directory-name", Alignment::Center), - } - } -} - -impl Component for MkdirPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseMkdirPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct NewfilePopup { - component: Input, -} - -impl NewfilePopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "New file name", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("file.txt", Alignment::Center), - } - } -} - -impl Component for NewfilePopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseNewFilePopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct OpenWithPopup { - component: Input, -} - -impl OpenWithPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "Open file with…", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Type the program to open the file with", Alignment::Center), - } - } -} - -impl Component for OpenWithPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => { - Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) - } - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseOpenWithPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct ProgressBarFull { - component: ProgressBar, -} - -impl ProgressBarFull { - pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { - Self { - component: ProgressBar::default() - .borders( - Borders::default() - .modifiers(BorderType::Rounded) - .sides(BorderSides::TOP | BorderSides::LEFT | BorderSides::RIGHT), - ) - .foreground(color) - .label(label) - .progress(prog) - .title(title, Alignment::Center), - } - } -} - -impl Component for ProgressBarFull { - fn on(&mut self, ev: Event) -> Option { - if matches!( - ev, - Event::Keyboard(KeyEvent { - code: Key::Char('c'), - modifiers: KeyModifiers::CONTROL - }) - ) { - Some(Msg::Transfer(TransferMsg::AbortTransfer)) - } else { - None - } - } -} - -#[derive(MockComponent)] -pub struct ProgressBarPartial { - component: ProgressBar, -} - -impl ProgressBarPartial { - pub fn new>(prog: f64, label: S, title: S, color: Color) -> Self { - Self { - component: ProgressBar::default() - .borders( - Borders::default() - .modifiers(BorderType::Rounded) - .sides(BorderSides::BOTTOM | BorderSides::LEFT | BorderSides::RIGHT), - ) - .foreground(color) - .label(label) - .progress(prog) - .title(title, Alignment::Center), - } - } -} - -impl Component for ProgressBarPartial { - fn on(&mut self, ev: Event) -> Option { - if matches!( - ev, - Event::Keyboard(KeyEvent { - code: Key::Char('c'), - modifiers: KeyModifiers::CONTROL - }) - ) { - Some(Msg::Transfer(TransferMsg::AbortTransfer)) - } else { - None - } - } -} - -#[derive(MockComponent)] -pub struct QuitPopup { - component: Radio, -} - -impl QuitPopup { - pub fn new(color: Color) -> Self { - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Yes", "No"]) - .title("Are you sure you want to quit termscp?", Alignment::Center), - } - } -} - -impl Component for QuitPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseQuitPopup)) - } - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::Quit)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::CloseQuitPopup)), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - if matches!( - self.perform(Cmd::Submit), - CmdResult::Submit(State::One(StateValue::Usize(0))) - ) { - Some(Msg::Ui(UiMsg::Quit)) - } else { - Some(Msg::Ui(UiMsg::CloseQuitPopup)) - } - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct RenamePopup { - component: Input, -} - -impl RenamePopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "/foo/bar/buzz.txt", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Move file(s) to…", Alignment::Center), - } - } -} - -impl Component for RenamePopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => { - Some(Msg::Transfer(TransferMsg::RenameFile(i))) - } - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseRenamePopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct ReplacePopup { - component: Radio, -} - -impl ReplacePopup { - pub fn new(filename: Option<&str>, color: Color) -> Self { - let text = match filename { - Some(f) => format!(r#"File "{f}" already exists. Overwrite file?"#), - None => "Overwrite files?".to_string(), - }; - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"]) - .title(text, Alignment::Center), - } - } -} - -impl Component for ReplacePopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) - } - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.perform(Cmd::Submit) { - CmdResult::Submit(State::One(StateValue::Usize(0))) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)) - } - CmdResult::Submit(State::One(StateValue::Usize(1))) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)) - } - CmdResult::Submit(State::One(StateValue::Usize(2))) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)) - } - CmdResult::Submit(State::One(StateValue::Usize(3))) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll)) - } - CmdResult::Submit(State::One(StateValue::Usize(4))) => { - Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) - } - _ => Some(Msg::None), - }, - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct SaveAsPopup { - component: Input, -} - -impl SaveAsPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "/foo/bar/buzz.txt", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title("Save as…", Alignment::Center), - } - } -} - -impl Component for SaveAsPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => { - Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) - } - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseSaveAsPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct SortingPopup { - component: Radio, -} - -impl SortingPopup { - pub fn new(value: FileSorting, color: Color) -> Self { - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Name", "Modify time", "Creation time", "Size"]) - .title("Sort files by…", Alignment::Center) - .value(match value { - FileSorting::CreationTime => 2, - FileSorting::ModifyTime => 1, - FileSorting::Name => 0, - FileSorting::Size => 3, - FileSorting::None => 0, - }), - } - } -} - -impl Component for SortingPopup { - fn on(&mut self, ev: Event) -> Option { - let result = match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => self.perform(Cmd::Move(Direction::Left)), - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => self.perform(Cmd::Move(Direction::Right)), - Event::Keyboard(KeyEvent { - code: Key::Esc | Key::Enter, - .. - }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), - _ => return None, - }; - if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { - Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { - 0 => FileSorting::Name, - 1 => FileSorting::ModifyTime, - 2 => FileSorting::CreationTime, - 3 => FileSorting::Size, - _ => FileSorting::Name, - }))) - } else { - Some(Msg::None) - } - } -} - -#[derive(MockComponent)] -pub struct StatusBarLocal { - component: Span, -} - -impl StatusBarLocal { - pub fn new(browser: &Browser, sorting_color: Color, hidden_color: Color) -> Self { - let file_sorting = file_sorting_label(browser.host_bridge().file_sorting); - let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible()); - Self { - component: Span::default().spans([ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(file_sorting).fg(sorting_color).reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(hidden_files).fg(hidden_color).reversed(), - ]), - } - } -} - -impl Component for StatusBarLocal { - fn on(&mut self, _ev: Event) -> Option { - None - } -} - -#[derive(MockComponent)] -pub struct StatusBarRemote { - component: Span, -} - -impl StatusBarRemote { - pub fn new( - browser: &Browser, - sorting_color: Color, - hidden_color: Color, - sync_color: Color, - ) -> Self { - let file_sorting = file_sorting_label(browser.remote().file_sorting); - let hidden_files = hidden_files_label(browser.remote().hidden_files_visible()); - let sync_browsing = match browser.sync_browsing { - true => "ON ", - false => "OFF", - }; - Self { - component: Span::default().spans([ - TextSpan::new("File sorting: ").fg(sorting_color), - TextSpan::new(file_sorting).fg(sorting_color).reversed(), - TextSpan::new(" Hidden files: ").fg(hidden_color), - TextSpan::new(hidden_files).fg(hidden_color).reversed(), - TextSpan::new(" Sync browsing: ").fg(sync_color), - TextSpan::new(sync_browsing).fg(sync_color).reversed(), - ]), - } - } -} - -impl Component for StatusBarRemote { - fn on(&mut self, _ev: Event) -> Option { - None - } -} - -fn file_sorting_label(sorting: FileSorting) -> &'static str { - match sorting { - FileSorting::CreationTime => "By creation time", - FileSorting::ModifyTime => "By modify time", - FileSorting::Name => "By name", - FileSorting::Size => "By size", - FileSorting::None => "", - } -} - -fn hidden_files_label(visible: bool) -> &'static str { - match visible { - true => "Show", - false => "Hide", - } -} - -#[derive(MockComponent)] -pub struct SymlinkPopup { - component: Input, -} - -impl SymlinkPopup { - pub fn new(color: Color) -> Self { - Self { - component: Input::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .input_type(InputType::Text) - .placeholder( - "Symlink name", - Style::default().fg(Color::Rgb(128, 128, 128)), - ) - .title( - "Create a symlink pointing to the selected entry", - Alignment::Center, - ), - } - } -} - -impl Component for SymlinkPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Delete, .. - }) => { - self.perform(Cmd::Cancel); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Backspace, - .. - }) => { - self.perform(Cmd::Delete); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Char(ch), - .. - }) => { - self.perform(Cmd::Type(ch)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => match self.state() { - State::One(StateValue::String(i)) => { - Some(Msg::Transfer(TransferMsg::CreateSymlink(i))) - } - _ => Some(Msg::None), - }, - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseSymlinkPopup)) - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct SyncBrowsingMkdirPopup { - component: Radio, -} - -impl SyncBrowsingMkdirPopup { - pub fn new(color: Color, dir_name: &str) -> Self { - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Yes", "No"]) - .title( - format!( - r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"# - ), - Alignment::Center, - ), - } - } -} - -impl Component for SyncBrowsingMkdirPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => Some(Msg::PendingAction( - PendingActionMsg::CloseSyncBrowsingMkdirPopup, - )), - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::PendingAction( - PendingActionMsg::CloseSyncBrowsingMkdirPopup, - )), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - if matches!( - self.perform(Cmd::Submit), - CmdResult::Submit(State::One(StateValue::Usize(0))) - ) { - Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)) - } else { - Some(Msg::PendingAction( - PendingActionMsg::CloseSyncBrowsingMkdirPopup, - )) - } - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct WaitPopup { - component: Paragraph, -} - -impl WaitPopup { - pub fn new>(text: S, color: Color) -> Self { - Self { - component: Paragraph::default() - .alignment(Alignment::Center) - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .text([TextSpan::from(text.as_ref())]) - .wrap(true), - } - } -} - -impl Component for WaitPopup { - fn on(&mut self, _ev: Event) -> Option { - None - } -} - -#[derive(MockComponent)] -pub struct WalkdirWaitPopup { - component: Paragraph, -} - -impl WalkdirWaitPopup { - pub fn new>(text: S, color: Color) -> Self { - Self { - component: Paragraph::default() - .alignment(Alignment::Center) - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .text([ - TextSpan::from(text.as_ref()), - TextSpan::from("Press 'CTRL+C' to abort"), - ]) - .wrap(true), - } - } -} - -impl Component for WalkdirWaitPopup { - fn on(&mut self, ev: Event) -> Option { - if matches!( - ev, - Event::Keyboard(KeyEvent { - code: Key::Char('c'), - modifiers: KeyModifiers::CONTROL - }) - ) { - Some(Msg::Transfer(TransferMsg::AbortWalkdir)) - } else { - None - } - } -} - -#[derive(MockComponent)] -pub struct WatchedPathsList { - component: List, -} - -impl WatchedPathsList { - pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self { - Self { - component: List::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .rewind(true) - .scroll(true) - .step(4) - .highlighted_color(color) - .highlighted_str("➤ ") - .title( - "These files are currently synched with the remote host", - Alignment::Center, - ) - .rows( - paths - .iter() - .map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())]) - .collect(), - ), - } - } -} - -impl Component for WatchedPathsList { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseWatchedPathsList)) - } - Event::Keyboard(KeyEvent { - code: Key::Down, .. - }) => { - self.perform(Cmd::Move(Direction::Down)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { - self.perform(Cmd::Move(Direction::Up)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::PageDown, - .. - }) => { - self.perform(Cmd::Scroll(Direction::Down)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::PageUp, .. - }) => { - self.perform(Cmd::Scroll(Direction::Up)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Home, .. - }) => { - self.perform(Cmd::GoTo(Position::Begin)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::End, .. }) => { - self.perform(Cmd::GoTo(Position::End)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - // get state - if let State::One(StateValue::Usize(idx)) = self.component.state() { - Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx))) - } else { - Some(Msg::None) - } - } - _ => None, - } - } -} - -#[derive(MockComponent)] -pub struct WatcherPopup { - component: Radio, -} - -impl WatcherPopup { - pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self { - let text = match watched { - false => format!(r#"Synchronize changes from "{local}" to "{remote}"?"#), - true => format!(r#"Stop synchronizing changes at "{local}"?"#), - }; - Self { - component: Radio::default() - .borders( - Borders::default() - .color(color) - .modifiers(BorderType::Rounded), - ) - .foreground(color) - .choices(["Yes", "No"]) - .title(text, Alignment::Center), - } - } -} - -impl Component for WatcherPopup { - fn on(&mut self, ev: Event) -> Option { - match ev { - Event::Keyboard(KeyEvent { - code: Key::Left, .. - }) => { - self.perform(Cmd::Move(Direction::Left)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { - code: Key::Right, .. - }) => { - self.perform(Cmd::Move(Direction::Right)); - Some(Msg::None) - } - Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { - Some(Msg::Ui(UiMsg::CloseWatcherPopup)) - } - Event::Keyboard(KeyEvent { - code: Key::Char('y'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Transfer(TransferMsg::ToggleWatch)), - Event::Keyboard(KeyEvent { - code: Key::Char('n'), - modifiers: KeyModifiers::NONE, - }) => Some(Msg::Ui(UiMsg::CloseWatcherPopup)), - Event::Keyboard(KeyEvent { - code: Key::Enter, .. - }) => { - if matches!( - self.perform(Cmd::Submit), - CmdResult::Submit(State::One(StateValue::Usize(0))) - ) { - Some(Msg::Transfer(TransferMsg::ToggleWatch)) - } else { - Some(Msg::Ui(UiMsg::CloseWatcherPopup)) - } - } - _ => None, - } - } -} +pub use self::keybindings::KeybindingsPopup; +pub use self::mkdir::{MkdirPopup, SyncBrowsingMkdirPopup}; +pub use self::newfile::NewfilePopup; +pub use self::open_with::OpenWithPopup; +pub use self::progress_bar::{ProgressBarFull, ProgressBarPartial}; +pub use self::quit::QuitPopup; +pub use self::rename::RenamePopup; +pub use self::replace::ReplacePopup; +pub use self::save_as::SaveAsPopup; +pub use self::sorting::SortingPopup; +pub use self::status_bar::{StatusBarLocal, StatusBarRemote}; +pub use self::symlink::SymlinkPopup; +pub use self::wait::{WaitPopup, WalkdirWaitPopup}; +pub use self::watcher::{WatchedPathsList, WatcherPopup}; diff --git a/src/ui/activities/filetransfer/components/popups/chmod.rs b/src/ui/activities/filetransfer/components/popups/chmod.rs index 0c81ff4..110df37 100644 --- a/src/ui/activities/filetransfer/components/popups/chmod.rs +++ b/src/ui/activities/filetransfer/components/popups/chmod.rs @@ -6,7 +6,7 @@ use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Col use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout}; use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; -use super::{Msg, TransferMsg, UiMsg}; +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; #[derive(Default, Clone, Copy, PartialEq, Eq)] pub enum Item { diff --git a/src/ui/activities/filetransfer/components/popups/copy.rs b/src/ui/activities/filetransfer/components/popups/copy.rs new file mode 100644 index 0000000..b081c9d --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/copy.rs @@ -0,0 +1,93 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct CopyPopup { + component: Input, +} + +impl CopyPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "destination", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title("Copy file(s) to\u{2026}", Alignment::Center), + } + } +} + +impl Component for CopyPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(i)) => { + Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) + } + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseCopyPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/delete.rs b/src/ui/activities/filetransfer/components/popups/delete.rs new file mode 100644 index 0000000..cef6bdb --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/delete.rs @@ -0,0 +1,72 @@ +use tui_realm_stdlib::Radio; +use tuirealm::command::{Cmd, CmdResult, Direction}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; + +#[derive(MockComponent)] +pub struct DeletePopup { + component: Radio, +} + +impl DeletePopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Yes", "No"]) + .value(1) + .title("Delete file(s)?", Alignment::Center), + } + } +} + +impl Component for DeletePopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Transfer(TransferMsg::DeleteFile)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::CloseDeletePopup)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Transfer(TransferMsg::DeleteFile)) + } else { + Some(Msg::Ui(UiMsg::CloseDeletePopup)) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/disconnect.rs b/src/ui/activities/filetransfer/components/popups/disconnect.rs new file mode 100644 index 0000000..be07732 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/disconnect.rs @@ -0,0 +1,71 @@ +use tui_realm_stdlib::Radio; +use tuirealm::command::{Cmd, CmdResult, Direction}; +use tuirealm::event::{Key, KeyEvent, KeyModifiers}; +use tuirealm::props::{Alignment, BorderType, Borders, Color}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct DisconnectPopup { + component: Radio, +} + +impl DisconnectPopup { + pub fn new(color: Color) -> Self { + Self { + component: Radio::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .choices(["Yes", "No"]) + .title("Are you sure you want to disconnect?", Alignment::Center), + } + } +} + +impl Component for DisconnectPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + Event::Keyboard(KeyEvent { + code: Key::Char('y'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::Disconnect)), + Event::Keyboard(KeyEvent { + code: Key::Char('n'), + modifiers: KeyModifiers::NONE, + }) => Some(Msg::Ui(UiMsg::CloseDisconnectPopup)), + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => { + if matches!( + self.perform(Cmd::Submit), + CmdResult::Submit(State::One(StateValue::Usize(0))) + ) { + Some(Msg::Ui(UiMsg::Disconnect)) + } else { + Some(Msg::Ui(UiMsg::CloseDisconnectPopup)) + } + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/error.rs b/src/ui/activities/filetransfer/components/popups/error.rs new file mode 100644 index 0000000..20444cf --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/error.rs @@ -0,0 +1,74 @@ +use tui_realm_stdlib::Paragraph; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct ErrorPopup { + component: Paragraph, +} + +impl ErrorPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text([TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for ErrorPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseErrorPopup)), + _ => None, + } + } +} + +#[derive(MockComponent)] +pub struct FatalPopup { + component: Paragraph, +} + +impl FatalPopup { + pub fn new>(text: S, color: Color) -> Self { + Self { + component: Paragraph::default() + .alignment(Alignment::Center) + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .text([TextSpan::from(text.as_ref())]) + .wrap(true), + } + } +} + +impl Component for FatalPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFatalPopup)), + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/file_info.rs b/src/ui/activities/filetransfer/components/popups/file_info.rs new file mode 100644 index 0000000..9328674 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/file_info.rs @@ -0,0 +1,122 @@ +use std::time::UNIX_EPOCH; + +use bytesize::ByteSize; +use remotefs::File; +use tui_realm_stdlib::List; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; +#[cfg(posix)] +use uzers::{get_group_by_gid, get_user_by_uid}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; +use crate::utils::fmt::fmt_time; + +#[derive(MockComponent)] +pub struct FileInfoPopup { + component: List, +} + +impl FileInfoPopup { + pub fn new(file: &File) -> Self { + let mut texts: TableBuilder = TableBuilder::default(); + // Abs path + let real_path = file.metadata().symlink.as_deref(); + let path: String = match real_path { + Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()), + None => format!("{}", file.path().display()), + }; + // Make texts + texts + .add_col(TextSpan::from("Path: ")) + .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); + texts + .add_row() + .add_col(TextSpan::from("Name: ")) + .add_col(TextSpan::new(file.name()).fg(Color::Yellow)); + if let Some(filetype) = file.extension() { + texts + .add_row() + .add_col(TextSpan::from("File type: ")) + .add_col(TextSpan::new(filetype).fg(Color::LightGreen)); + } + let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size); + texts + .add_row() + .add_col(TextSpan::from("Size: ")) + .add_col(TextSpan::new(format!("{bsize} ({size})").as_str()).fg(Color::Cyan)); + let atime: String = fmt_time( + file.metadata().accessed.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); + let ctime: String = fmt_time( + file.metadata().created.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); + let mtime: String = fmt_time( + file.metadata().modified.unwrap_or(UNIX_EPOCH), + "%b %d %Y %H:%M:%S", + ); + texts + .add_row() + .add_col(TextSpan::from("Creation time: ")) + .add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); + texts + .add_row() + .add_col(TextSpan::from("Last modified time: ")) + .add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); + texts + .add_row() + .add_col(TextSpan::from("Last access time: ")) + .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); + // User + #[cfg(posix)] + let username: String = match file.metadata().uid { + Some(uid) => match get_user_by_uid(uid) { + Some(user) => user.name().to_string_lossy().to_string(), + None => uid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(win)] + let username: String = format!("{}", file.metadata().uid.unwrap_or(0)); + // Group + #[cfg(posix)] + let group: String = match file.metadata().gid { + Some(gid) => match get_group_by_gid(gid) { + Some(group) => group.name().to_string_lossy().to_string(), + None => gid.to_string(), + }, + None => String::from("0"), + }; + #[cfg(win)] + let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); + texts + .add_row() + .add_col(TextSpan::from("User: ")) + .add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); + texts + .add_row() + .add_col(TextSpan::from("Group: ")) + .add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(false) + .title(file.name(), Alignment::Left) + .rows(texts.build()), + } + } +} + +impl Component for FileInfoPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Esc | Key::Enter, + .. + }) => Some(Msg::Ui(UiMsg::CloseFileInfoPopup)), + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/filter.rs b/src/ui/activities/filetransfer/components/popups/filter.rs new file mode 100644 index 0000000..0fd39ae --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/filter.rs @@ -0,0 +1,94 @@ +use tui_realm_stdlib::Input; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct FilterPopup { + component: Input, +} + +impl FilterPopup { + pub fn new(color: Color) -> Self { + Self { + component: Input::default() + .borders( + Borders::default() + .color(color) + .modifiers(BorderType::Rounded), + ) + .foreground(color) + .input_type(InputType::Text) + .placeholder( + "regex or wildmatch", + Style::default().fg(Color::Rgb(128, 128, 128)), + ) + .title( + "Filter files by regex or wildmatch in the current directory", + Alignment::Center, + ), + } + } +} + +impl Component for FilterPopup { + fn on(&mut self, ev: Event) -> Option { + match ev { + Event::Keyboard(KeyEvent { + code: Key::Left, .. + }) => { + self.perform(Cmd::Move(Direction::Left)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Right, .. + }) => { + self.perform(Cmd::Move(Direction::Right)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Home, .. + }) => { + self.perform(Cmd::GoTo(Position::Begin)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { code: Key::End, .. }) => { + self.perform(Cmd::GoTo(Position::End)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Delete, .. + }) => { + self.perform(Cmd::Cancel); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Backspace, + .. + }) => { + self.perform(Cmd::Delete); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Char(ch), + .. + }) => { + self.perform(Cmd::Type(ch)); + Some(Msg::None) + } + Event::Keyboard(KeyEvent { + code: Key::Enter, .. + }) => match self.state() { + State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))), + _ => Some(Msg::None), + }, + Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { + Some(Msg::Ui(UiMsg::CloseFilterPopup)) + } + _ => None, + } + } +} diff --git a/src/ui/activities/filetransfer/components/popups/keybindings.rs b/src/ui/activities/filetransfer/components/popups/keybindings.rs new file mode 100644 index 0000000..72ff900 --- /dev/null +++ b/src/ui/activities/filetransfer/components/popups/keybindings.rs @@ -0,0 +1,204 @@ +use tui_realm_stdlib::List; +use tuirealm::command::{Cmd, Direction, Position}; +use tuirealm::event::{Key, KeyEvent}; +use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; +use tuirealm::{Component, Event, MockComponent, NoUserEvent}; + +use crate::ui::activities::filetransfer::{Msg, UiMsg}; + +#[derive(MockComponent)] +pub struct KeybindingsPopup { + component: List, +} + +impl KeybindingsPopup { + pub fn new(key_color: Color) -> Self { + Self { + component: List::default() + .borders(Borders::default().modifiers(BorderType::Rounded)) + .scroll(true) + .step(8) + .highlighted_str("? ") + .title("Keybindings", Alignment::Center) + .rewind(true) + .rows( + TableBuilder::default() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Disconnect")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to previous directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change explorer tab")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Move up/down in list")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Enter directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Upload/Download file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Switch between explorer and log window", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Toggle hidden files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Change file sorting mode")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Copy")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Make directory")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Search files")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Go to path")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Show help")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Show info about selected file", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Create symlink pointing to the current selected entry", + )) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Reload directory content")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Select file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from(" Create new file")) + .add_row() + .add_col(TextSpan::new("").bold().fg(key_color)) + .add_col(TextSpan::from( + " Open text file with preferred editor", + )) + .add_row() + .add_col(TextSpan::new("