├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── copy.md │ ├── feature_request.md │ ├── question.md │ └── security.md ├── PULL_REQUEST_TEMPLATE.md ├── actions-rs │ └── grcov.yml └── workflows │ ├── build-artifacts.yml │ ├── install.yml │ ├── linux.yml │ ├── macos.yml │ ├── stale.yml │ ├── website.yml │ └── windows.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── images │ ├── auth.gif │ ├── bookmarks.gif │ ├── config.gif │ ├── explorer.gif │ ├── flags │ ├── br.png │ ├── cn.png │ ├── de.png │ ├── dk.png │ ├── es.png │ ├── fr.png │ ├── gb.png │ ├── it.png │ ├── jp.png │ ├── kr.png │ ├── nl.png │ ├── ru.png │ └── us.png │ ├── termscp-128.png │ ├── termscp-512.png │ ├── termscp-64.png │ ├── termscp-96.png │ ├── termscp.svg │ ├── text-editor.gif │ └── themes.gif ├── build.rs ├── dist ├── build │ ├── README.md │ ├── aarch64_centos7 │ │ └── Dockerfile │ ├── aarch64_debian10 │ │ └── Dockerfile │ ├── freebsd.sh │ ├── linux-aarch64.sh │ ├── linux-x86_64.sh │ ├── macos.sh │ ├── windows.ps1 │ ├── x86_64_centos7 │ │ └── Dockerfile │ └── x86_64_debian12 │ │ └── Dockerfile ├── deb.sh └── rpm.sh ├── docs ├── de │ ├── README.md │ └── man.md ├── developer.md ├── es │ ├── README.md │ └── man.md ├── fr │ ├── README.md │ └── man.md ├── it │ ├── README.md │ └── man.md ├── man.md ├── misc │ └── README.deb.txt ├── pt-BR │ ├── README.md │ └── man.md └── zh-CN │ ├── README.md │ └── man.md ├── install.sh ├── rustfmt.toml ├── site ├── assets │ ├── images │ │ ├── explorer.gif │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── og_preview.jpg │ │ └── termscp.webp │ └── videos │ │ └── explorer.mp4 ├── changelog.html ├── css │ └── markdown.css ├── get-started.html ├── html │ ├── components │ │ ├── footer.html │ │ └── menu.html │ ├── get-started.html │ ├── home.html │ └── updates.html ├── index.html ├── input.css ├── js │ ├── core.js │ ├── events.js │ ├── lang.min.js │ └── resolvers.js ├── lang │ ├── en.json │ ├── es.json │ ├── fr.json │ ├── it.json │ └── zh-CN.json ├── output.css ├── robots.txt ├── sitemap.xml ├── updates.html └── user-manual.html ├── src ├── activity_manager.rs ├── cli.rs ├── cli │ └── remote.rs ├── config │ ├── bookmarks.rs │ ├── bookmarks │ │ ├── aws_s3.rs │ │ ├── kube.rs │ │ └── smb.rs │ ├── mod.rs │ ├── params.rs │ ├── serialization.rs │ └── themes.rs ├── explorer │ ├── builder.rs │ ├── formatter.rs │ └── mod.rs ├── filetransfer │ ├── host_bridge_builder.rs │ ├── mod.rs │ ├── params.rs │ ├── params │ │ ├── aws_s3.rs │ │ ├── kube.rs │ │ ├── smb.rs │ │ └── webdav.rs │ └── remotefs_builder.rs ├── host │ ├── bridge.rs │ ├── localhost.rs │ ├── mod.rs │ ├── remote_bridged.rs │ └── remote_bridged │ │ └── temp_mapped_file.rs ├── main.rs ├── support.rs ├── system │ ├── auto_update.rs │ ├── bookmarks_client.rs │ ├── config_client.rs │ ├── environment.rs │ ├── keys │ │ ├── filestorage.rs │ │ ├── keyringstorage.rs │ │ └── mod.rs │ ├── logging.rs │ ├── mod.rs │ ├── notifications.rs │ ├── sshkey_storage.rs │ ├── theme_provider.rs │ └── watcher │ │ ├── change.rs │ │ └── mod.rs ├── ui │ ├── activities │ │ ├── auth │ │ │ ├── bookmarks.rs │ │ │ ├── components │ │ │ │ ├── bookmarks.rs │ │ │ │ ├── form.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── popup.rs │ │ │ │ └── text.rs │ │ │ ├── misc.rs │ │ │ ├── mod.rs │ │ │ ├── update.rs │ │ │ └── view.rs │ │ ├── filetransfer │ │ │ ├── actions │ │ │ │ ├── change_dir.rs │ │ │ │ ├── chmod.rs │ │ │ │ ├── copy.rs │ │ │ │ ├── delete.rs │ │ │ │ ├── edit.rs │ │ │ │ ├── exec.rs │ │ │ │ ├── filter.rs │ │ │ │ ├── find.rs │ │ │ │ ├── mark.rs │ │ │ │ ├── mkdir.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── newfile.rs │ │ │ │ ├── open.rs │ │ │ │ ├── pending.rs │ │ │ │ ├── rename.rs │ │ │ │ ├── save.rs │ │ │ │ ├── scan.rs │ │ │ │ ├── submit.rs │ │ │ │ ├── symlink.rs │ │ │ │ ├── walkdir.rs │ │ │ │ └── watcher.rs │ │ │ ├── components │ │ │ │ ├── log.rs │ │ │ │ ├── misc.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── popups.rs │ │ │ │ ├── popups │ │ │ │ │ ├── chmod.rs │ │ │ │ │ └── goto.rs │ │ │ │ ├── selected_files.rs │ │ │ │ └── transfer │ │ │ │ │ ├── file_list.rs │ │ │ │ │ ├── file_list_with_search.rs │ │ │ │ │ └── mod.rs │ │ │ ├── fswatcher.rs │ │ │ ├── lib │ │ │ │ ├── browser.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── transfer.rs │ │ │ │ └── walkdir.rs │ │ │ ├── misc.rs │ │ │ ├── mod.rs │ │ │ ├── session.rs │ │ │ ├── update.rs │ │ │ └── view.rs │ │ ├── mod.rs │ │ └── setup │ │ │ ├── actions.rs │ │ │ ├── components │ │ │ ├── commons.rs │ │ │ ├── config.rs │ │ │ ├── mod.rs │ │ │ ├── ssh.rs │ │ │ └── theme.rs │ │ │ ├── config.rs │ │ │ ├── mod.rs │ │ │ ├── update.rs │ │ │ └── view │ │ │ ├── mod.rs │ │ │ ├── setup.rs │ │ │ ├── ssh_keys.rs │ │ │ └── theme.rs │ ├── context.rs │ ├── mod.rs │ └── store.rs └── utils │ ├── crypto.rs │ ├── file.rs │ ├── fmt.rs │ ├── mod.rs │ ├── parser.rs │ ├── path.rs │ ├── random.rs │ ├── ssh.rs │ ├── string.rs │ ├── test_helpers.rs │ ├── tty.rs │ └── ui.rs ├── tailwind.config.js └── themes ├── catppuccin-frappe.toml ├── catppuccin-latte.toml ├── catppuccin-macchiato.toml ├── catppuccin-moka.toml ├── default.toml ├── earth-wind-fire.toml ├── horizon.toml ├── mono-bright.toml ├── mono-dark.toml ├── sugarplum.toml ├── ubuntu.toml └── veeso.toml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report of the bug you've encountered 4 | title: "[BUG] - ISSUE_TITLE" 5 | labels: bug 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## Steps to reproduce 15 | 16 | Steps to reproduce the bug you encountered 17 | 18 | ## Expected behaviour 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ## Environment 23 | 24 | - OS: [e.g. GNU/Linux Debian 10] 25 | - Architecture [Arm, x86_64, ...] 26 | - Rust version 27 | - termscp version 28 | - Protocol used 29 | - Remote server version and name 30 | 31 | ## Log 32 | 33 | Report the snippet of the log file containing the unexpected behaviour. 34 | If there is any information you consider to be confidential, shadow it. 35 | 36 | ## Additional information 37 | 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/copy.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Copy 3 | about: Report a typo/error in a repository document 4 | title: "[COPY] - ISSUE_TITLE" 5 | labels: documentation 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Report 11 | 12 | ### DOCUMENT NAME 13 | 14 | This sentence at row ROW_NUMBER doesn't seem right: 15 | 16 | > Write down here the wrong sentence 17 | 18 | and I think it should be changed to: 19 | 20 | > Write down here the correct sentence 21 | 22 | `Copy paste the template above for all the sentences to fix` 23 | 24 | --- 25 | 26 | `Copy paste the template above for all the documents to fix` 27 | 28 | ## Additional information 29 | 30 | > ❗ Report LANGUAGE checks only if it concerns the documents above 31 | > ❗ If the documents concerns more than one language, copy paste the checks below for each check you want to report 32 | > ❗ The PR mention regards the indicated language. If you check the box, I may add you to a PR where I need to translate a new section of the user manual/README or other documents. I promise I won't stress you anyway. 33 | 34 | - [ ] I am C1/C2 speaker for this language: LANGUAGE 35 | - [ ] You can mention me in a PR in case a review for translations is needed 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea to improve termscp 4 | title: "[Feature Request] - FEATURE_TITLE" 5 | labels: "new feature" 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | Put here a brief introduction to your suggestion. 13 | 14 | ### Changes 15 | 16 | The following changes to the application are expected 17 | 18 | - ... 19 | 20 | ## Implementation 21 | 22 | Provide any kind of suggestion you propose on how to implement the feature. 23 | If you have none, delete this section. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask what you want about the project 4 | title: "[QUESTION] - TITLE" 5 | labels: question 6 | assignees: veeso 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/security.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Security report 3 | about: Create a report of a security vulnerability 4 | title: "[SECURITY] - ISSUE_TITLE" 5 | labels: security 6 | assignees: veeso 7 | 8 | --- 9 | 10 | ## Description 11 | 12 | Severity: 13 | 14 | - [ ] **critical** 15 | - [ ] high 16 | - [ ] medium 17 | - [ ] low 18 | 19 | A clear and concise description of the security vulnerability. 20 | 21 | ## Additional information 22 | 23 | Add any other context about the problem here. 24 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # ISSUE _NUMBER_ - PULL_REQUEST_TITLE 2 | 3 | Fixes # (issue) 4 | 5 | ## Description 6 | 7 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 8 | 9 | List here your changes 10 | 11 | - I made this... 12 | - I made also that... 13 | 14 | ## Type of change 15 | 16 | Please select relevant options. 17 | 18 | - [ ] Bug fix (non-breaking change which fixes an issue) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 21 | - [ ] This change requires a documentation update 22 | 23 | ## Checklist 24 | 25 | - [ ] My code follows the contribution guidelines of this project 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] My changes generate no new warnings 29 | - [ ] I formatted the code with `cargo fmt` 30 | - [ ] I checked my code using `cargo clippy` and reports no warnings 31 | - [ ] I have added tests that prove my fix is effective or that my feature works 32 | - [ ] I have introduced no new *C-bindings* 33 | - [ ] The changes I've made are Windows, MacOS, UNIX, Linux compatible (or I've handled them using `cfg target_os`) 34 | - [ ] I increased or maintained the code coverage for the project, compared to the previous commit 35 | 36 | ## Acceptance tests 37 | 38 | wait for a *project maintainer* to fulfill this section... 39 | 40 | - [ ] regression test: ... 41 | -------------------------------------------------------------------------------- /.github/actions-rs/grcov.yml: -------------------------------------------------------------------------------- 1 | branch: false 2 | ignore-not-existing: true 3 | llvm: true 4 | output-type: lcov 5 | ignore: 6 | - "/*" 7 | - "C:/*" 8 | - "../*" 9 | - src/main.rs 10 | - src/activity_manager.rs 11 | - src/cli_opts.rs 12 | - src/support.rs 13 | - src/system/notifications.rs 14 | - "src/ui/activities/*" 15 | - src/ui/context.rs 16 | - src/ui/input.rs 17 | -------------------------------------------------------------------------------- /.github/workflows/build-artifacts.yml: -------------------------------------------------------------------------------- 1 | name: "Build artifacts" 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | TERMSCP_VERSION: "0.17.0" 8 | 9 | jobs: 10 | build-binaries: 11 | name: Build - ${{ matrix.platform.release_for }} 12 | strategy: 13 | matrix: 14 | platform: 15 | - release_for: MacOS-x86_64 16 | os: macos-latest 17 | target: x86_64-apple-darwin 18 | script: macos.sh 19 | 20 | - release_for: MacOS-M1 21 | os: macos-latest 22 | target: aarch64-apple-darwin 23 | script: macos.sh 24 | 25 | runs-on: ${{ matrix.platform.os }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: dtolnay/rust-toolchain@stable 29 | with: 30 | toolchain: stable 31 | targets: ${{ matrix.platform.target }} 32 | - name: Build release 33 | run: cargo build --release --target ${{ matrix.platform.target }} 34 | - name: Prepare artifact files 35 | run: | 36 | mkdir -p .artifact 37 | mv target/${{ matrix.platform.target }}/release/termscp .artifact/termscp 38 | tar -czf .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz -C .artifact termscp 39 | ls -l .artifact/ 40 | - name: "Upload artifact" 41 | uses: actions/upload-artifact@v4 42 | with: 43 | if-no-files-found: error 44 | retention-days: 1 45 | name: termscp-${{ matrix.platform.target }} 46 | path: .artifact/termscp-v${{ env.TERMSCP_VERSION }}-${{ matrix.platform.target }}.tar.gz 47 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | name: Install.sh 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: 7 | - "v*" 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install dependencies 19 | run: sudo apt update && sudo apt install -y curl wget libsmbclient 20 | - name: Install termscp from script 21 | run: | 22 | ./install.sh -v=0.12.3 -f 23 | which termscp || exit 1 24 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "*.md" 7 | - "./site/**/*" 8 | push: 9 | paths-ignore: 10 | - "*.md" 11 | - "./site/**/*" 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Install dependencies 23 | run: sudo apt update && sudo apt install -y libdbus-1-dev libsmbclient-dev 24 | - uses: dtolnay/rust-toolchain@stable 25 | with: 26 | toolchain: stable 27 | components: rustfmt, clippy 28 | - name: Run tests 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: test 32 | args: --no-default-features --features github-actions --no-fail-fast 33 | - name: Format 34 | run: cargo fmt --all -- --check 35 | - name: Clippy 36 | run: cargo clippy -- -Dwarnings 37 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "*.md" 7 | - "./site/**/*" 8 | push: 9 | paths-ignore: 10 | - "*.md" 11 | - "./site/**/*" 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: macos-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: 23 | toolchain: stable 24 | components: rustfmt, clippy 25 | - name: Build 26 | run: cargo build 27 | - name: Run tests 28 | run: cargo test --verbose --features github-actions 29 | - name: Clippy 30 | run: cargo clippy -- -Dwarnings 31 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: "30 1 * * *" 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v4.1.1 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 7 17 | stale-issue-label: "stale" 18 | stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." 19 | close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale." 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | exempt-issue-labels: "backlog" 24 | exempt-all-milestones: true 25 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | paths: 9 | - "./site/**/*" 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: read 17 | pages: write 18 | id-token: write 19 | 20 | # Allow one concurrent deployment 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | # Single deploy job since we're just deploying 27 | deploy: 28 | environment: 29 | name: github-pages 30 | url: ${{ steps.deployment.outputs.page_url }} 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v3 35 | - name: Setup Pages 36 | uses: actions/configure-pages@v5 37 | - name: Upload artifact 38 | uses: actions/upload-pages-artifact@v3 39 | with: 40 | path: "./site/" 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - "*.md" 7 | - "./site/**/*" 8 | push: 9 | paths-ignore: 10 | - "*.md" 11 | - "./site/**/*" 12 | 13 | env: 14 | CARGO_TERM_COLOR: always 15 | 16 | jobs: 17 | build: 18 | runs-on: windows-2019 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: dtolnay/rust-toolchain@stable 23 | with: 24 | toolchain: stable 25 | components: rustfmt, clippy 26 | - name: Build 27 | run: cargo build 28 | - name: Run tests 29 | run: cargo test --verbose --features github-actions 30 | - name: Clippy 31 | run: cargo clippy -- -Dwarnings 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .rpm/ 3 | 4 | # Created by https://www.gitignore.io/api/rust 5 | # Edit at https://www.gitignore.io/?templates=rust 6 | 7 | ### Rust ### 8 | # Generated by Cargo 9 | # will have compiled files and executables 10 | /target/ 11 | 12 | # These are backup files generated by rustfmt 13 | **/*.rs.bk 14 | 15 | # End of https://www.gitignore.io/api/rust 16 | 17 | # Distributions 18 | *.rpm 19 | *.deb 20 | dist/pkgs/arch/*.tar.gz 21 | 22 | # Macos 23 | .DS_Store 24 | 25 | dist/pkgs/ 26 | dist/build/macos/openssl/ 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Christian Visintin "] 3 | categories = ["command-line-utilities"] 4 | description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV" 5 | edition = "2024" 6 | homepage = "https://termscp.veeso.dev" 7 | include = ["src/**/*", "build.rs", "LICENSE", "README.md", "CHANGELOG.md"] 8 | keywords = ["terminal", "ftp", "scp", "sftp", "tui"] 9 | license = "MIT" 10 | name = "termscp" 11 | readme = "README.md" 12 | repository = "https://github.com/veeso/termscp" 13 | version = "0.17.0" 14 | rust-version = "1.85.0" 15 | 16 | [package.metadata.rpm] 17 | package = "termscp" 18 | 19 | [package.metadata.rpm.cargo] 20 | buildflags = ["--release"] 21 | 22 | [package.metadata.rpm.targets] 23 | termscp = { path = "/usr/bin/termscp" } 24 | 25 | [package.metadata.deb] 26 | maintainer = "Christian Visintin " 27 | copyright = "2025, Christian Visintin " 28 | extended-description-file = "docs/misc/README.deb.txt" 29 | 30 | [[bin]] 31 | name = "termscp" 32 | path = "src/main.rs" 33 | 34 | [dependencies] 35 | argh = "^0.1" 36 | bitflags = "^2" 37 | bytesize = "^2" 38 | chrono = "^0.4" 39 | content_inspector = "^0.2" 40 | dirs = "^6" 41 | edit = "^0.1" 42 | filetime = "^0.2" 43 | hostname = "^0.4" 44 | keyring = { version = "^3", features = [ 45 | "apple-native", 46 | "windows-native", 47 | "sync-secret-service", 48 | "vendored", 49 | ] } 50 | lazy-regex = "^3" 51 | lazy_static = "^1" 52 | log = "^0.4" 53 | magic-crypt = "4" 54 | notify = "8" 55 | notify-rust = { version = "^4", default-features = false, features = ["d"] } 56 | nucleo = "0.5" 57 | open = "^5.0" 58 | rand = "^0.9" 59 | regex = "^1" 60 | remotefs = "^0.3" 61 | remotefs-aws-s3 = "0.4" 62 | remotefs-kube = "0.4" 63 | remotefs-webdav = "^0.2" 64 | rpassword = "^7" 65 | self_update = { version = "^0.42", default-features = false, features = [ 66 | "rustls", 67 | "archive-tar", 68 | "archive-zip", 69 | "compression-flate2", 70 | "compression-zip-deflate", 71 | ] } 72 | serde = { version = "^1", features = ["derive"] } 73 | simplelog = "^0.12" 74 | ssh2-config = "^0.4" 75 | tempfile = "3" 76 | thiserror = "2" 77 | tokio = { version = "1.44", features = ["rt"] } 78 | toml = "^0.8" 79 | tui-realm-stdlib = "2" 80 | tuirealm = "2" 81 | unicode-width = "^0.2" 82 | version-compare = "^0.2" 83 | whoami = "^1.5" 84 | wildmatch = "^2" 85 | 86 | [target."cfg(not(target_os = \"macos\"))".dependencies] 87 | remotefs-smb = { version = "^0.3", optional = true } 88 | 89 | [target."cfg(target_family = \"unix\")".dependencies] 90 | remotefs-ftp = { version = "^0.2", features = ["vendored", "native-tls"] } 91 | remotefs-ssh = { version = "^0.6", features = ["ssh2-vendored"] } 92 | uzers = "0.12" 93 | 94 | [target."cfg(target_family = \"windows\")".dependencies] 95 | remotefs-ftp = { version = "^0.2", features = ["native-tls"] } 96 | remotefs-ssh = { version = "^0.6" } 97 | 98 | [dev-dependencies] 99 | pretty_assertions = "^1" 100 | serial_test = "^3" 101 | 102 | [build-dependencies] 103 | cfg_aliases = "0.2" 104 | vergen-git2 = { version = "1", features = ["build", "cargo", "rustc", "si"] } 105 | 106 | [features] 107 | default = ["smb", "keyring"] 108 | github-actions = [] 109 | isolated-tests = [] 110 | keyring = [] 111 | smb = ["dep:remotefs-smb"] 112 | smb-vendored = ["remotefs-smb/vendored"] 113 | 114 | [profile.dev] 115 | incremental = true 116 | 117 | [profile.release] 118 | strip = true 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Christian Visintin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only latst version of termscp has the latest security updates. 6 | Because of that, **you should always consider updating termscp to the latest version**. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | If you have any security vulnerability or concern to report, please open an issue using the `Security report` template. 11 | -------------------------------------------------------------------------------- /assets/images/auth.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/auth.gif -------------------------------------------------------------------------------- /assets/images/bookmarks.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/bookmarks.gif -------------------------------------------------------------------------------- /assets/images/config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/config.gif -------------------------------------------------------------------------------- /assets/images/explorer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/explorer.gif -------------------------------------------------------------------------------- /assets/images/flags/br.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/br.png -------------------------------------------------------------------------------- /assets/images/flags/cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/cn.png -------------------------------------------------------------------------------- /assets/images/flags/de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/de.png -------------------------------------------------------------------------------- /assets/images/flags/dk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/dk.png -------------------------------------------------------------------------------- /assets/images/flags/es.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/es.png -------------------------------------------------------------------------------- /assets/images/flags/fr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/fr.png -------------------------------------------------------------------------------- /assets/images/flags/gb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/gb.png -------------------------------------------------------------------------------- /assets/images/flags/it.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/it.png -------------------------------------------------------------------------------- /assets/images/flags/jp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/jp.png -------------------------------------------------------------------------------- /assets/images/flags/kr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/kr.png -------------------------------------------------------------------------------- /assets/images/flags/nl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/nl.png -------------------------------------------------------------------------------- /assets/images/flags/ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/ru.png -------------------------------------------------------------------------------- /assets/images/flags/us.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/flags/us.png -------------------------------------------------------------------------------- /assets/images/termscp-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/termscp-128.png -------------------------------------------------------------------------------- /assets/images/termscp-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/termscp-512.png -------------------------------------------------------------------------------- /assets/images/termscp-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/termscp-64.png -------------------------------------------------------------------------------- /assets/images/termscp-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/termscp-96.png -------------------------------------------------------------------------------- /assets/images/termscp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/text-editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/text-editor.gif -------------------------------------------------------------------------------- /assets/images/themes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/assets/images/themes.gif -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use cfg_aliases::cfg_aliases; 2 | use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder, SysinfoBuilder}; 3 | 4 | fn main() -> Result<(), Box> { 5 | // Setup cfg aliases 6 | cfg_aliases! { 7 | // Platforms 8 | macos: { target_os = "macos" }, 9 | linux: { target_os = "linux" }, 10 | posix: { target_family = "unix" }, 11 | win: { target_family = "windows" }, 12 | // exclusive features 13 | smb: { all(feature = "smb", not( macos )) }, 14 | smb_unix: { all(unix, feature = "smb", not(macos)) }, 15 | smb_windows: { all(windows, feature = "smb") } 16 | } 17 | 18 | let build = BuildBuilder::all_build()?; 19 | let cargo = CargoBuilder::all_cargo()?; 20 | let git2 = Git2Builder::all_git()?; 21 | let rustc = RustcBuilder::all_rustc()?; 22 | let si = SysinfoBuilder::all_sysinfo()?; 23 | 24 | Emitter::default() 25 | .add_instructions(&build)? 26 | .add_instructions(&cargo)? 27 | .add_instructions(&git2)? 28 | .add_instructions(&rustc)? 29 | .add_instructions(&si)? 30 | .emit()?; 31 | 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /dist/build/README.md: -------------------------------------------------------------------------------- 1 | # Build with Docker 2 | 3 | - [Build with Docker](#build-with-docker) 4 | - [Prerequisites](#prerequisites) 5 | - [Build](#build) 6 | 7 | --- 8 | 9 | ## Prerequisites 10 | 11 | - Docker 12 | 13 | ## Build 14 | 15 | 1. Build x86_64 16 | 17 | this will build termscp for: 18 | 19 | - Linux x86_64 Deb packages 20 | - Linux x86_64 RPM packages 21 | - Windows x86_64 MSVC packages 22 | 23 | ```sh 24 | 25 | ``` 26 | -------------------------------------------------------------------------------- /dist/build/aarch64_centos7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:centos7 as builder 2 | 3 | WORKDIR /usr/src/ 4 | # Install dependencies 5 | RUN yum -y install \ 6 | git \ 7 | gcc \ 8 | pkgconfig \ 9 | gcc \ 10 | make \ 11 | dbus-devel \ 12 | libsmbclient-devel \ 13 | bash \ 14 | rpm-build 15 | # Install rust 16 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \ 17 | chmod +x /tmp/rust.sh && \ 18 | /tmp/rust.sh -y 19 | # Clone repository 20 | RUN git clone https://github.com/veeso/termscp.git 21 | # Set workdir to termscp 22 | WORKDIR /usr/src/termscp/ 23 | # Install cargo rpm 24 | RUN source $HOME/.cargo/env && cargo install cargo-rpm 25 | 26 | ENTRYPOINT ["tail", "-f", "/dev/null"] 27 | -------------------------------------------------------------------------------- /dist/build/aarch64_debian10/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | 3 | WORKDIR /usr/src/ 4 | # Install dependencies 5 | RUN apt update && apt install -y \ 6 | git \ 7 | gcc \ 8 | pkg-config \ 9 | libdbus-1-dev \ 10 | build-essential \ 11 | libsmbclient-dev \ 12 | libgit2-dev \ 13 | build-essential \ 14 | pkg-config \ 15 | libbsd-dev \ 16 | libcap-dev \ 17 | libcups2-dev \ 18 | libgnutls28-dev \ 19 | libicu-dev \ 20 | libjansson-dev \ 21 | libkeyutils-dev \ 22 | libldap2-dev \ 23 | zlib1g-dev \ 24 | libpam0g-dev \ 25 | libacl1-dev \ 26 | libarchive-dev \ 27 | flex \ 28 | bison \ 29 | libntirpc-dev \ 30 | libtracker-sparql-3.0-dev \ 31 | libglib2.0-dev \ 32 | libdbus-1-dev \ 33 | libsasl2-dev \ 34 | libunistring-dev \ 35 | bash \ 36 | curl \ 37 | cpanminus && \ 38 | cpanm Parse::Yapp::Driver; 39 | 40 | # Install rust 41 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \ 42 | chmod +x /tmp/rust.sh && \ 43 | /tmp/rust.sh -y 44 | # Clone repository 45 | RUN git clone https://github.com/veeso/termscp.git 46 | # Set workdir to termscp 47 | WORKDIR /usr/src/termscp/ 48 | # Install cargo deb 49 | RUN . $HOME/.cargo/env && cargo install cargo-deb 50 | 51 | ENTRYPOINT ["tail", "-f", "/dev/null"] 52 | -------------------------------------------------------------------------------- /dist/build/freebsd.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: freebsd.sh " 5 | exit 1 6 | fi 7 | 8 | VERSION=$1 9 | 10 | set -e # Don't fail 11 | 12 | # Go to root dir 13 | cd ../../ 14 | # Check if in correct directory 15 | if [ ! -f Cargo.toml ]; then 16 | echo "Please start freebsd.sh from dist/build/ directory" 17 | exit 1 18 | fi 19 | 20 | # Build release 21 | cargo build --release && cargo strip 22 | # Make pkg 23 | cd target/release/ 24 | PKG="termscp-v${VERSION}-x86_64-unknown-freebsd.tar.gz" 25 | tar czf $PKG termscp 26 | sha256sum $PKG 27 | # Calc sha256 of exec and copy to path 28 | HASH=`sha256sum termscp | cut -d ' ' -f1` 29 | sudo cp termscp /usr/local/bin/termscp 30 | mkdir -p ../../dist/pkgs/freebsd/ 31 | mv $PKG ../../dist/pkgs/freebsd/$PKG 32 | cd ../../dist/pkgs/freebsd/ 33 | rm manifest 34 | echo -e "name: \"termscp\"" > manifest 35 | echo -e "version: $VERSION" >> manifest 36 | echo -e "origin: veeso/termscp" >> manifest 37 | echo -e "comment: \"A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/Kube/S3/WebDAV\"" >> manifest 38 | echo -e "desc: <> manifest 52 | 53 | exit $? 54 | -------------------------------------------------------------------------------- /dist/build/linux-aarch64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: $0 [branch] [--no-cache]" 5 | exit 1 6 | fi 7 | 8 | VERSION=$1 9 | 10 | if [ -z "$2" ]; then 11 | BRANCH=$VERSION 12 | else 13 | BRANCH=$2 14 | fi 15 | 16 | CACHE="" 17 | 18 | if [ "$3" == "--no-cache" ]; then 19 | CACHE="--no-cache" 20 | fi 21 | 22 | # names 23 | ARM64_DEB_NAME="termscp-arm64_deb" 24 | 25 | docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 26 | 27 | set -e # Don't fail 28 | 29 | # Create pkgs directory 30 | cd .. 31 | PKGS_DIR=$(pwd)/pkgs 32 | cd - 33 | mkdir -p ${PKGS_DIR}/ 34 | # Build aarch64_deb 35 | cd aarch64_debian10/ 36 | docker buildx build --platform linux/arm64 $CACHE --build-arg branch=${BRANCH} --tag $ARM64_DEB_NAME . 37 | cd - 38 | mkdir -p ${PKGS_DIR}/deb/ 39 | mkdir -p ${PKGS_DIR}/aarch64-unknown-linux-gnu/ 40 | docker run --name "$ARM64_DEB_NAME" -d "$ARM64_DEB_NAME" || docker start "$ARM64_DEB_NAME" 41 | docker exec -it "$ARM64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb" 42 | docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_arm64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_arm64.deb 43 | docker cp ${ARM64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/aarch64-unknown-linux-gnu/ 44 | docker stop "$ARM64_DEB_NAME" 45 | # Make tar.gz 46 | cd ${PKGS_DIR}/aarch64-unknown-linux-gnu/ 47 | tar cvzf termscp-v${VERSION}-aarch64-unknown-linux-gnu.tar.gz termscp 48 | echo "Sha256 (homebrew aarch64): $(sha256sum termscp-v${VERSION}-aarch64-unknown-linux-gnu.tar.gz)" 49 | rm termscp 50 | cd - 51 | 52 | exit $? 53 | -------------------------------------------------------------------------------- /dist/build/linux-x86_64.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Usage: $0 [branch] [--no-cache]" 5 | exit 1 6 | fi 7 | 8 | VERSION=$1 9 | 10 | if [ -z "$2" ]; then 11 | BRANCH=$VERSION 12 | else 13 | BRANCH=$2 14 | fi 15 | 16 | CACHE="" 17 | 18 | if [ "$3" == "--no-cache" ]; then 19 | CACHE="--no-cache" 20 | fi 21 | 22 | # names 23 | X86_64_DEB_NAME="termscp-x86_64_deb" 24 | 25 | set -e # Don't fail 26 | 27 | # Create pkgs directory 28 | cd .. 29 | PKGS_DIR=$(pwd)/pkgs 30 | cd - 31 | mkdir -p ${PKGS_DIR}/ 32 | # Build x86_64_deb 33 | cd x86_64_debian12/ 34 | docker build $CACHE --build-arg branch=${BRANCH} --tag "$X86_64_DEB_NAME" . 35 | cd - 36 | mkdir -p ${PKGS_DIR}/deb/ 37 | mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/ 38 | docker run --name "$X86_64_DEB_NAME" -d "$X86_64_DEB_NAME" || docker start "$X86_64_DEB_NAME" 39 | docker exec -it "$X86_64_DEB_NAME" bash -c ". \$HOME/.cargo/env && git fetch origin && git checkout origin/$BRANCH && cargo build --release --features smb-vendored && cargo deb" 40 | docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}-1_amd64.deb ${PKGS_DIR}/deb/termscp_${VERSION}_amd64.deb 41 | docker cp ${X86_64_DEB_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/ 42 | docker stop "$X86_64_DEB_NAME" 43 | # Make tar.gz 44 | cd ${PKGS_DIR}/x86_64-unknown-linux-gnu/ 45 | tar cvzf termscp-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz termscp 46 | echo "Sha256 x86_64 (homebrew): $(sha256sum termscp-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz)" 47 | rm termscp 48 | cd - 49 | 50 | exit $? 51 | -------------------------------------------------------------------------------- /dist/build/macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | make_pkg() { 4 | ARCH=$1 5 | VERSION=$2 6 | TARGET_DIR="$3" 7 | if [ -z "$TARGET_DIR" ]; then 8 | TARGET_DIR=target/release/ 9 | fi 10 | ROOT_DIR=$(pwd) 11 | cd "$TARGET_DIR" 12 | PKG="termscp-v${VERSION}-${ARCH}-apple-darwin.tar.gz" 13 | tar czf "$PKG" termscp 14 | HASH=$(sha256sum "$PKG") 15 | mkdir -p "${ROOT_DIR}/dist/pkgs/macos/" 16 | mv "$PKG" "${ROOT_DIR}/dist/pkgs/macos/$PKG" 17 | cd - 18 | echo "$HASH" 19 | } 20 | 21 | detect_platform() { 22 | local platform 23 | platform="$(uname -s | tr '[:upper:]' '[:lower:]')" 24 | 25 | case "${platform}" in 26 | linux) platform="linux" ;; 27 | darwin) platform="macos" ;; 28 | freebsd) platform="freebsd" ;; 29 | esac 30 | 31 | printf '%s' "${platform}" 32 | } 33 | 34 | detect_arch() { 35 | local arch 36 | arch="$(uname -m | tr '[:upper:]' '[:lower:]')" 37 | 38 | case "${arch}" in 39 | amd64) arch="x86_64" ;; 40 | armv*) arch="arm" ;; 41 | arm64) arch="aarch64" ;; 42 | esac 43 | 44 | # `uname -m` in some cases mis-reports 32-bit OS as 64-bit, so double check 45 | if [ "${arch}" = "x86_64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then 46 | arch="i686" 47 | elif [ "${arch}" = "aarch64" ] && [ "$(getconf LONG_BIT)" -eq 32 ]; then 48 | arch="arm" 49 | fi 50 | 51 | printf '%s' "${arch}" 52 | } 53 | 54 | if [ -z "$1" ]; then 55 | echo "Usage: macos.sh " 56 | exit 1 57 | fi 58 | 59 | PLATFORM="$(detect_platform)" 60 | ARCH="$(detect_arch)" 61 | 62 | if [ "$PLATFORM" != "macos" ]; then 63 | echo "macos build is only available on MacOs systems" 64 | exit 1 65 | fi 66 | 67 | VERSION=$1 68 | export BUILD_ROOT 69 | BUILD_ROOT="$(pwd)/../../" 70 | 71 | set -e # Don't fail 72 | 73 | # Go to root dir 74 | cd ../../ 75 | # Check if in correct directory 76 | if [ ! -f Cargo.toml ]; then 77 | echo "Please start macos.sh from dist/build/ directory" 78 | exit 1 79 | fi 80 | 81 | # Build release (x86_64) 82 | X86_TARGET="" 83 | X86_TARGET_DIR="" 84 | if [ "$ARCH" = "aarch64" ]; then 85 | X86_TARGET="--target x86_64-apple-darwin" 86 | X86_TARGET_DIR="target/x86_64-apple-darwin/release/" 87 | fi 88 | cargo build --release $X86_TARGET 89 | # Make pkg 90 | X86_64_HASH=$(make_pkg "x86_64" "$VERSION" $X86_TARGET_DIR) 91 | RET_X86_64=$? 92 | 93 | ARM64_TARGET="" 94 | ARM64_TARGET_DIR="" 95 | if [ "$ARCH" = "aarch64" ]; then 96 | ARM64_TARGET="--target aarch64-apple-darwin" 97 | ARM64_TARGET_DIR="target/aarch64-apple-darwin/release/" 98 | fi 99 | cd "$BUILD_ROOT" 100 | # Build ARM64 pkg 101 | cargo build --release $ARM64_TARGET 102 | # Make pkg 103 | ARM64_HASH=$(make_pkg "arm64" "$VERSION" $ARM64_TARGET_DIR) 104 | RET_ARM64=$? 105 | 106 | echo "x86_64 hash: $X86_64_HASH" 107 | echo "arm64 hash: $ARM64_HASH" 108 | 109 | [ "$RET_ARM64" -eq 0 ] && [ "$RET_X86_64" -eq 0 ] 110 | exit $? 111 | -------------------------------------------------------------------------------- /dist/build/windows.ps1: -------------------------------------------------------------------------------- 1 | $ErrorActionPreference = 'Stop'; 2 | 3 | if ($args.Count -eq 0) { 4 | Write-Output "Usage: windows.ps1 " 5 | exit 1 6 | } 7 | 8 | $version = $args[0] 9 | 10 | # Go to root directory 11 | Set-Location ..\..\ 12 | # Build 13 | cargo build --release 14 | # Make zip 15 | $zipName = "termscp-v$version-x86_64-pc-windows-msvc.zip" 16 | Set-Location .\target\release\ 17 | Compress-Archive -Force termscp.exe $zipName 18 | # Get checksum 19 | Get-FileHash $zipName 20 | Move-Item $zipName .\..\..\dist\pkgs\windows\$zipName 21 | -------------------------------------------------------------------------------- /dist/build/x86_64_centos7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM centos:centos7 as builder 2 | 3 | WORKDIR /usr/src/ 4 | # Install dependencies 5 | RUN yum -y install \ 6 | git \ 7 | gcc \ 8 | pkgconfig \ 9 | gcc \ 10 | make \ 11 | dbus-devel \ 12 | libsmbclient-devel \ 13 | bash \ 14 | rpm-build 15 | # Install rust 16 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \ 17 | chmod +x /tmp/rust.sh && \ 18 | /tmp/rust.sh -y 19 | # Clone repository 20 | RUN git clone https://github.com/veeso/termscp.git 21 | # Set workdir to termscp 22 | WORKDIR /usr/src/termscp/ 23 | # Install cargo rpm 24 | RUN source $HOME/.cargo/env && cargo install cargo-rpm 25 | 26 | ENTRYPOINT ["tail", "-f", "/dev/null"] 27 | -------------------------------------------------------------------------------- /dist/build/x86_64_debian12/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm 2 | 3 | WORKDIR /usr/src/ 4 | # Install dependencies 5 | RUN apt update && apt install -y \ 6 | git \ 7 | gcc \ 8 | pkg-config \ 9 | libdbus-1-dev \ 10 | build-essential \ 11 | libsmbclient-dev \ 12 | libgit2-dev \ 13 | build-essential \ 14 | pkg-config \ 15 | libbsd-dev \ 16 | libcap-dev \ 17 | libcups2-dev \ 18 | libgnutls28-dev \ 19 | libgnutls30 \ 20 | libicu-dev \ 21 | libjansson-dev \ 22 | libkeyutils-dev \ 23 | libldap2-dev \ 24 | zlib1g-dev \ 25 | libpam0g-dev \ 26 | libacl1-dev \ 27 | libarchive-dev \ 28 | libssl-dev \ 29 | flex \ 30 | bison \ 31 | libntirpc-dev \ 32 | libglib2.0-dev \ 33 | libdbus-1-dev \ 34 | libsasl2-dev \ 35 | libunistring-dev \ 36 | bash \ 37 | curl \ 38 | cpanminus && \ 39 | cpanm Parse::Yapp::Driver; 40 | 41 | # Install rust 42 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \ 43 | chmod +x /tmp/rust.sh && \ 44 | /tmp/rust.sh -y && \ 45 | . $HOME/.cargo/env && \ 46 | cargo version 47 | # Clone repository 48 | RUN git clone https://github.com/veeso/termscp.git 49 | # Set workdir to termscp 50 | WORKDIR /usr/src/termscp/ 51 | # Install cargo deb 52 | RUN . $HOME/.cargo/env && cargo install cargo-deb 53 | 54 | ENTRYPOINT ["tail", "-f", "/dev/null"] 55 | -------------------------------------------------------------------------------- /dist/deb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Installing cargo-deb..." 4 | cargo install cargo-deb 5 | if [ ! -f "Cargo.toml" ]; then 6 | echo "Yout must be in the project root directory" 7 | exit 1 8 | fi 9 | echo "Running cargo-deb" 10 | cargo deb 11 | exit $? 12 | -------------------------------------------------------------------------------- /dist/rpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | which rpmbuild > /dev/null 4 | if [ $? -ne 0 ]; then 5 | echo "You must install rpmbuild on your machine" 6 | fi 7 | echo "Installing cargo-rpm..." 8 | cargo install cargo-rpm 9 | if [ ! -f "Cargo.toml" ]; then 10 | echo "Yout must be in the project root directory" 11 | exit 1 12 | fi 13 | echo "Running cargo-rpm" 14 | cargo rpm init 15 | cargo rpm build 16 | exit $? 17 | -------------------------------------------------------------------------------- /docs/misc/README.deb.txt: -------------------------------------------------------------------------------- 1 | Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/Kube/S3/WebDAV. 2 | Basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and 3 | to interact with the local file system. 4 | 5 | Features: 6 | 7 | - 📁 Different communication protocols 8 | - SFTP 9 | - SCP 10 | - FTP and FTPS 11 | - S3 12 | - 🖥 Explore and operate on the remote and on the local machine file system with a handy UI 13 | - Create, remove, rename, search, view and edit files 14 | - ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections 15 | - 📝 View and edit files with your favourite applications 16 | - 💁 SFTP/SCP authentication with SSH keys and username/password 17 | - 🐧 Compatible with Windows, Linux, FreeBSD and MacOS 18 | - 🎨 Make it yours! 19 | - Themes 20 | - Custom file explorer format 21 | - Customizable text editor 22 | - Customizable file sorting 23 | - and many other parameters... 24 | - 📫 Get notified via Desktop Notifications when a large file has been transferred 25 | - 🔐 Save your password in your operating system key vault 26 | - 🦀 Rust-powered 27 | - 👀 Developed keeping an eye on performance 28 | - 🦄 Frequent awesome updates 29 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | imports_granularity = "Module" 3 | -------------------------------------------------------------------------------- /site/assets/images/explorer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/explorer.gif -------------------------------------------------------------------------------- /site/assets/images/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/favicon-16x16.png -------------------------------------------------------------------------------- /site/assets/images/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/favicon-32x32.png -------------------------------------------------------------------------------- /site/assets/images/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/favicon-96x96.png -------------------------------------------------------------------------------- /site/assets/images/og_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/og_preview.jpg -------------------------------------------------------------------------------- /site/assets/images/termscp.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/images/termscp.webp -------------------------------------------------------------------------------- /site/assets/videos/explorer.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veeso/termscp/8715c2b6f9a93c4378e3dfb276d7211688c5115d/site/assets/videos/explorer.mp4 -------------------------------------------------------------------------------- /site/changelog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV | termscp 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /site/css/markdown.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | .markdown a { 6 | color: dodgerblue; 7 | text-decoration: none; 8 | } 9 | 10 | .markdown a:hover { 11 | text-decoration: underline; 12 | } 13 | 14 | .markdown p { 15 | font-size: 1.1em; 16 | } 17 | 18 | .markdown h1 { 19 | font-size: 2em; 20 | } 21 | 22 | .markdown h2 { 23 | font-size: 1.6em; 24 | } 25 | 26 | .markdown h3 { 27 | font-size: 1.4em; 28 | } 29 | 30 | .markdown h4 { 31 | font-size: 1.2em; 32 | } 33 | 34 | .markdown img { 35 | display: none; 36 | } 37 | 38 | @media (min-width: 600px) { 39 | .markdown img { 40 | display: block; 41 | width: 60%; 42 | margin-left: 20%; 43 | } 44 | } 45 | 46 | .markdown blockquote { 47 | border-left: 0.25em solid #ccc; 48 | font-size: 90%; 49 | padding: 0.1em; 50 | padding-left: 0.5em; 51 | } 52 | 53 | .markdown pre code { 54 | background-color: inherit; 55 | font-size: 100%; 56 | } 57 | 58 | .markdown code { 59 | background-color: #eee; 60 | border-radius: 6px; 61 | font-size: 85%; 62 | padding: 0.2em 0.4em; 63 | } 64 | 65 | :is(.dark) .markdown code { 66 | background-color: #404040; 67 | } 68 | 69 | .markdown table { 70 | border-collapse: collapse; 71 | border-spacing: 0; 72 | display: block; 73 | height: fit-content; 74 | max-width: 100%; 75 | overflow: auto; 76 | width: max-content; 77 | } 78 | 79 | .markdown table tr { 80 | border-top: 1px solid #c6cbd1; 81 | } 82 | 83 | .markdown table td, 84 | .markdown table th { 85 | border: 1px solid #c6cbd1; 86 | padding: 6px 13px; 87 | } 88 | -------------------------------------------------------------------------------- /site/get-started.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | get started with termscp | termscp 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /site/html/components/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 7 |
8 |
9 | 11 |
12 |
13 | 15 |
16 |
17 | 19 |
20 |
21 |
22 | 23 |

24 | P.IVA IT03104140300 25 |

26 |

27 | Via Antonio Marangoni 33, 33100, Udine (UD) 28 |

29 | 30 |

31 | Christian Visintin © 32 |  |  33 | Privacy policy 34 |  |  35 | Cookie policy 36 |

37 |
38 |
-------------------------------------------------------------------------------- /site/html/home.html: -------------------------------------------------------------------------------- 1 | 2 |
4 |

termscp

5 | logo 6 |

7 | A feature rich terminal UI file transfer and explorer with support for 8 | SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV 9 |

10 | 13 |
14 |

15 | termscp 0.17.0 is NOW out! Download it from  16 | here! 17 |

18 |
19 |
20 |
21 |

Handy UI

22 |

23 | Explore and operate on the remote and on the local machine file system 24 | with a handy UI. 25 |

26 |
27 |
28 |

Cross platform

29 |

30 | Runs on Windows, MacOS, Linux and BSD 31 |

32 |
33 |
34 |

Customizable

35 |

36 | Customize the file explorer, the text editor to use and default 37 | options 38 |

39 |
40 |
41 |

Bookmarks

42 |

43 | Connect to your favourite hosts through built-in bookmarks and recent 44 | connections support 45 |

46 |
47 |
48 |

Security first

49 |

50 | Save your password into your operating system key vault 51 |

52 |
53 |
54 |

Eye on performance

55 |

56 | termscp has been developed keeping an eye on performance to prevent 57 | cpu usage 58 |

59 |
60 |
61 |
62 | 65 |
66 |
67 |
68 |
69 |

70 | Get 71 | started 72 |

73 |
74 |
75 |

76 | User manual 77 |

78 |
79 |
80 |

81 | Install 82 | updates 83 |

84 |
85 |
86 |
-------------------------------------------------------------------------------- /site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | termscp is a terminal file transfer and explorer for SCP/SFTP/FTP/Kube/S3/WebDAV/SMB/WebDAV | termscp 7 | 8 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /site/input.css: -------------------------------------------------------------------------------- 1 | @import "css/markdown.css"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @font-face { 8 | font-family: "Sora"; 9 | font-style: normal; 10 | font-weight: 300; 11 | font-display: swap; 12 | src: url(https://fonts.gstatic.com/s/sora/v11/xMQ9uFFYT72X5wkB_18qmnndmSdSnx2BAfO5mnuyOo1l_iMwWa-xsaQ.woff2) 13 | format("woff2"); 14 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 15 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 16 | } 17 | /* latin */ 18 | @font-face { 19 | font-family: "Sora"; 20 | font-style: normal; 21 | font-weight: 300; 22 | font-display: swap; 23 | src: url(https://fonts.gstatic.com/s/sora/v11/xMQ9uFFYT72X5wkB_18qmnndmSdSnx2BAfO5mnuyOo1l_iMwV6-x.woff2) 24 | format("woff2"); 25 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 26 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 27 | U+FEFF, U+FFFD; 28 | } 29 | /* latin-ext */ 30 | @font-face { 31 | font-family: "Sora"; 32 | font-style: normal; 33 | font-weight: 400; 34 | font-display: swap; 35 | src: url(https://fonts.gstatic.com/s/sora/v11/xMQ9uFFYT72X5wkB_18qmnndmSdSnx2BAfO5mnuyOo1l_iMwWa-xsaQ.woff2) 36 | format("woff2"); 37 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 38 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 39 | } 40 | /* latin */ 41 | @font-face { 42 | font-family: "Sora"; 43 | font-style: normal; 44 | font-weight: 400; 45 | font-display: swap; 46 | src: url(https://fonts.gstatic.com/s/sora/v11/xMQ9uFFYT72X5wkB_18qmnndmSdSnx2BAfO5mnuyOo1l_iMwV6-x.woff2) 47 | format("woff2"); 48 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 49 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 50 | U+FEFF, U+FFFD; 51 | } 52 | 53 | html { 54 | scroll-behavior: smooth; 55 | } 56 | 57 | body { 58 | font-family: "Sora", sans-serif; 59 | margin: 0; 60 | min-width: 100vw; 61 | overflow-x: hidden; 62 | padding: 0; 63 | } 64 | 65 | main { 66 | padding-top: 5rem; 67 | } 68 | 69 | p { 70 | font-weight: 300; 71 | } 72 | 73 | input { 74 | font-family: "Sora", sans-serif; 75 | font-size: 1em; 76 | } 77 | 78 | textarea { 79 | font-family: "Sora", sans-serif; 80 | } 81 | 82 | h1, 83 | h2, 84 | h3, 85 | h4, 86 | h5, 87 | h6 { 88 | margin-top: 0; 89 | margin-bottom: 0.5rem; 90 | } 91 | 92 | button { 93 | box-sizing: border-box; 94 | cursor: pointer; 95 | } 96 | 97 | button, 98 | input { 99 | margin: 0; 100 | font-family: inherit; 101 | font-size: inherit; 102 | line-height: inherit; 103 | } 104 | 105 | a { 106 | text-decoration: none; 107 | } 108 | 109 | a:hover { 110 | text-decoration: none; 111 | } 112 | 113 | pre { 114 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 115 | padding: 16px; 116 | overflow: auto; 117 | font-size: 85%; 118 | line-height: 1.45; 119 | color: #d0d0d0; 120 | background-color: #222629; 121 | border-radius: 3px; 122 | word-wrap: normal; 123 | border-radius: 0.5em; 124 | } 125 | 126 | pre .function { 127 | color: #f08d49; 128 | } 129 | 130 | pre .string { 131 | color: #7ec699; 132 | } 133 | -------------------------------------------------------------------------------- /site/js/core.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description return navigator language. If language is not supported default will be returned 3 | * @returns {string} 4 | */ 5 | 6 | function getNavigatorLanguage() { 7 | let lang = navigator.language; 8 | // Complete lang 9 | if (languageSupported(lang)) { 10 | return lang; 11 | } 12 | // Reduced lang 13 | lang = lang.split(/[-_]/)[0] || "en"; 14 | if (!languageSupported(lang)) { 15 | return "en"; 16 | } 17 | return lang; 18 | } 19 | 20 | /** 21 | * @description check whether provided language is supported by the website 22 | * @param {string} lang 23 | * @returns {boolean} 24 | */ 25 | function languageSupported(lang) { 26 | return ["en", "zh-CN", "it", "fr", "es"].includes(lang); 27 | } 28 | 29 | /** 30 | * @description update website language 31 | * @param {string} lang 32 | */ 33 | function setSiteLanguage(lang) { 34 | setLanguage(lang); 35 | } 36 | 37 | const converter = new showdown.Converter({ tables: true }); 38 | 39 | /** 40 | * @description load page associated to hash 41 | * @param {string} hash 42 | */ 43 | function loadPage(path) { 44 | switch (path) { 45 | case "/": 46 | case "/index.html": 47 | loadHtml("home.html"); 48 | break; 49 | case "/get-started.html": 50 | loadHtml("get-started.html"); 51 | break; 52 | case "/user-manual.html": 53 | loadUserManual(); 54 | break; 55 | case "/updates.html": 56 | loadHtml("updates.html"); 57 | break; 58 | case "/changelog.html": 59 | loadMarkdown( 60 | "https://raw.githubusercontent.com/veeso/termscp/main/CHANGELOG.md" 61 | ); 62 | break; 63 | } 64 | } 65 | 66 | function loadHtml(page) { 67 | const url = "html/" + page; 68 | $("#main").load(url, function () { 69 | onPageLoaded(); 70 | }); 71 | } 72 | 73 | function loadMenu() { 74 | $("#menu").load("html/components/menu.html", function () { 75 | onPageLoaded(); 76 | }); 77 | } 78 | 79 | function loadFooter() { 80 | $("#footer").load("html/components/footer.html", function () { 81 | onPageLoaded(); 82 | }); 83 | } 84 | 85 | function loadMarkdown(page) { 86 | getMarkdown(page, function (md) { 87 | const div = jQuery("
", { 88 | id: page, 89 | class: "container markdown", 90 | }); 91 | div.html(converter.makeHtml(md)); 92 | $("#main").empty(); 93 | $("#main").append(div); 94 | onPageLoaded(); 95 | }); 96 | } 97 | 98 | /** 99 | * @description get markdown and pass result to onLoaded 100 | * @param {string} url 101 | * @param {function} onLoaded 102 | */ 103 | function getMarkdown(url, onLoaded) { 104 | $.ajax({ 105 | url, 106 | type: "GET", 107 | dataType: "text", 108 | success: onLoaded, 109 | }); 110 | } 111 | 112 | function loadUserManual() { 113 | // Load language 114 | const lang = getNavigatorLanguage(); 115 | if (lang === "en") { 116 | loadMarkdown( 117 | `https://raw.githubusercontent.com/veeso/termscp/main/docs/man.md` 118 | ); 119 | } else { 120 | loadMarkdown( 121 | `https://raw.githubusercontent.com/veeso/termscp/main/docs/${lang}/man.md` 122 | ); 123 | } 124 | } 125 | 126 | // startup 127 | $(function () { 128 | loadPage(window.location.pathname); 129 | loadMenu(); 130 | loadFooter(); 131 | }); 132 | -------------------------------------------------------------------------------- /site/js/events.js: -------------------------------------------------------------------------------- 1 | function onPageLoaded() { 2 | reloadTranslations(); 3 | setThemeToggle(); 4 | setTheme(getTheme()); 5 | } 6 | 7 | function onToggleMenu() { 8 | const mobileMenu = $("#mobile-menu"); 9 | let wasVisible = false; 10 | // if not visible set flex and slide in, otherwise slide out 11 | if (!mobileMenu.is(":visible")) { 12 | mobileMenu.css("display", "flex"); 13 | mobileMenu.addClass("animate__animated animate__slideInLeft"); 14 | } else { 15 | mobileMenu.addClass("animate__animated animate__slideOutLeft"); 16 | wasVisible = true; 17 | } 18 | 19 | // on animation end remove animation, if visible set hidden 20 | mobileMenu.on("animationend", () => { 21 | mobileMenu.removeClass( 22 | "animate__animated animate__slideOutLeft animate__slideInLeft" 23 | ); 24 | if (wasVisible) { 25 | mobileMenu.css("display", "none"); 26 | } 27 | mobileMenu.off("animationend"); 28 | }); 29 | } 30 | 31 | function getTheme() { 32 | const theme = localStorage.getItem("theme"); 33 | 34 | if (!theme) { 35 | return window.matchMedia("(prefers-color-scheme: dark)").matches 36 | ? "theme-dark" 37 | : "theme-light"; 38 | } 39 | 40 | return theme; 41 | } 42 | 43 | function setThemeToggle() { 44 | if (getTheme() === "theme-dark") { 45 | $("#theme-toggle-dark-icon").css("display", "block"); 46 | $("#theme-toggle-light-icon").css("display", "none"); 47 | } else { 48 | $("#theme-toggle-dark-icon").css("display", "none"); 49 | $("#theme-toggle-light-icon").css("display", "block"); 50 | } 51 | } 52 | 53 | // function to set a given theme/color-scheme 54 | function setTheme(themeName) { 55 | localStorage.setItem("theme", themeName); 56 | if (themeName === "theme-dark") { 57 | document.documentElement.classList.add("dark"); 58 | } else { 59 | document.documentElement.classList.remove("dark"); 60 | } 61 | setThemeToggle(); 62 | } 63 | 64 | // function to toggle between light and dark theme 65 | function toggleTheme() { 66 | console.log("theme", getTheme()); 67 | if (getTheme() === "theme-dark") { 68 | setTheme("theme-light"); 69 | } else { 70 | setTheme("theme-dark"); 71 | } 72 | } 73 | 74 | // Startup 75 | $(function () { 76 | // Init language 77 | setSiteLanguage(getNavigatorLanguage()); 78 | 79 | // init theme 80 | setTheme(getTheme()); 81 | }); 82 | -------------------------------------------------------------------------------- /site/js/lang.min.js: -------------------------------------------------------------------------------- 1 | var currentLanguage=null;var languagePath="lang/";var currentLanguageDict=null;function setLanguage(lang){currentLanguage=lang;const jsonFile=languagePath+currentLanguage+".json";$.getJSON(jsonFile,function(langData){currentLanguageDict=flatDict(langData);reloadTranslations()})}function reloadTranslations(){$("[translate]").each(function(){const translationAttr=$(this).attr("translate");$(this).text(getInstantTranslation(translationAttr))})}function getInstantTranslation(key){if(currentLanguageDict!==null&&key in currentLanguageDict){return currentLanguageDict[key]}else{return"{{ "+key+" }}"}}function flatDict(dict){const iterNode=(flatten,path,node)=>{for(const key of Object.keys(node)){const child=node[key];const childKey=path?path+"."+key:key;if(typeof child==="object"){flatten=iterNode(flatten,childKey,child)}else{flatten[childKey]=child}}return flatten};return iterNode({},null,dict)} -------------------------------------------------------------------------------- /site/js/resolvers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description resolve copyright year 3 | */ 4 | 5 | function resolveCopyright() { 6 | const year = new Date().getFullYear(); 7 | $("[resolve-copyright]").each(function () { 8 | $(this).text(year); 9 | }); 10 | } 11 | 12 | /** 13 | * @description resolve video fallback source in case fails. Uses an image instead 14 | */ 15 | function resolveVideoFallback() { 16 | $("[resolve-video-fallback]").each(function () { 17 | const fallback = $(this).attr("resolve-video-fallback"); 18 | // Add listener 19 | $(this).on("error", function () { 20 | const image = document.createElement("img"); 21 | image.src = fallback; 22 | image.classList = ["preview"]; 23 | $(this).parent().replaceWith(image); 24 | }); 25 | }); 26 | } 27 | 28 | // init 29 | $(function () { 30 | resolveCopyright(); 31 | resolveVideoFallback(); 32 | }); 33 | -------------------------------------------------------------------------------- /site/lang/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "desc": "功能丰富的终端文件传输", 4 | "intro": "介绍", 5 | "getStarted": "开始", 6 | "updates": "安装更新", 7 | "manual": "用户手册", 8 | "changelog": "发布历史", 9 | "author": "关于作者", 10 | "support": "支持我" 11 | }, 12 | "intro": { 13 | "caption": "功能丰富的终端 UI 文件传输和浏览器,支持 SCP/SFTP/FTP/Kube/S3/WebDAV", 14 | "getStarted": "开始 →", 15 | "versionAlert": "termscp 0.17.0 现已发布! 从下载", 16 | "here": "这里", 17 | "features": { 18 | "handy": { 19 | "title": "方便的用户界面", 20 | "body": "使用方便的 UI 在远程和本地机器文件系统上探索和操作。" 21 | }, 22 | "crossPlatform": { 23 | "title": "跨平台", 24 | "body": "在 Windows、MacOS、Linux 和 FreeBSD 上运行" 25 | }, 26 | "customizable": { 27 | "title": "可定制", 28 | "body": "自定义文件浏览器、UI 和许多其他参数..." 29 | }, 30 | "bookmarks": { 31 | "title": "书签", 32 | "body": "通过内置书签和最近的连接连接到您最喜欢的主机" 33 | }, 34 | "security": { 35 | "title": "安全第一", 36 | "body": "将您的密码保存到您的操作系统密钥保管库中" 37 | }, 38 | "performance": { 39 | "title": "关注性能", 40 | "body": "已经开发了 termscp 关注性能以防止高 CPU 使用率" 41 | } 42 | }, 43 | "footer": { 44 | "getStarted": "开始", 45 | "manual": "用户手册", 46 | "updates": "安装更新" 47 | } 48 | }, 49 | "getStarted": { 50 | "title": "开始", 51 | "quickSetup": "快速设置", 52 | "suggested": "我们强烈建议使用这种方法来安装termscp", 53 | "posixUsers": "如果您是 Linux、FreeBSD 或 MacOS 用户,您可以通过这个简单的命令安装 termscp,它将使用 shell 脚本安装程序:", 54 | "windows": { 55 | "title": "Windows 用户", 56 | "intro": "安装", 57 | "moderation": "考虑到 Chocolatey 审核自上次发布以来可能需要长达数周的时间,因此如果最新版本尚不可用,您可以从以下位置下载 ZIP 文件进行安装", 58 | "then": "然后,从 ZIP 目录,安装它" 59 | }, 60 | "linuxUsers": "Linux 用户", 61 | "notConfident": "如果您对使用 shell 脚本没有信心,请选择这些方法", 62 | "noBinary": "如果您的平台的二进制文件不可用,请改用此方法", 63 | "arch": { 64 | "title": "Arch派生用户", 65 | "intro": "在基于 Arch Linux 的发行版上,您可以使用 AUR 包管理器安装 termscp,例如", 66 | "then": "然后运行" 67 | }, 68 | "debian": { 69 | "title": "Debian派生用户", 70 | "body": "在基于 Debian 的发行版上,您可以通过以下方式使用 Deb 包安装 termscp:" 71 | }, 72 | "redhat": { 73 | "title": "Redhat派生用户", 74 | "body": "在基于 RedHat 的发行版上,您可以通过以下方式使用 RPM 包安装 termscp:" 75 | }, 76 | "macos": { 77 | "title": "MacOS 用户", 78 | "install": "通过以下方式安装termscp:" 79 | }, 80 | "cargo": { 81 | "title": "使用“Cargo”安装", 82 | "body": "如果您的系统没有可用的软件包,您可以选择通过以下方式安装 termscp:", 83 | "requirements": "要通过 Cargo 安装termscp,必须满足这些要求", 84 | "install": "然后你可以通过安装termscp", 85 | "noKeyring": "或者,如果您不想支持密钥环,或者您正在构建 *BSD:", 86 | "noSMB": "或者如果你想禁用 SMB :" 87 | } 88 | }, 89 | "updates": { 90 | "title": "使termscp保持最新", 91 | "disclaimer": " 使用此方法更新 termscp 仅适用于 0.7.x 版本或更高版本。 如果您有旧版本,则必须使用", 92 | "reasons": { 93 | "title": "为什么要安装更新", 94 | "wallOfText": "termscp 是一个仍处于早期开发阶段的应用程序,第一个版本已于 2020 年 12 月发布,实际上只有一个人在开发它,为了改进它并使其成功,还有很多工作要做快速可靠。除此之外,您还应该考虑到,由于它是一个使用网络协议的应用程序,旨在操纵机密和凭据,因此可能始终存在安全问题。我不能保证我这几个月发布的版本没有安全问题,如果有,它们甚至可能不是我的错,但它们可能包含在 termscp 依赖的库中。因此,保持termscp 最新总是非常重要的。为了证明我有多关心它,请考虑一下我已经实现了许多其他开源应用程序不会做的事情:更新检查。每当您启动termscp(除非在配置中停用)termscp 将始终检查是否有新版本可用并立即通知您。除了安全问题,每次重大更新都会带来许多很棒的功能🦄你不能错过,每次更新后应用程序都变得更加可靠和稳定😄", 95 | "tldr": "可能的安全问题; 新的很棒的功能; 性能和稳定性; Bug修复" 96 | }, 97 | "gui": { 98 | "title": "图形用户界面方式", 99 | "body": "GUI 方法只包括启动没有选项的 termscp,然后您应该位于身份验证表单的前面。 如果有可用更新,则会显示类似\"termscp x.y.z is OUT! Update and read release notes with CTRL+R\"之类的消息。 此时更新termscp所需要做的就是:", 100 | "steps": { 101 | "st": "按 CTRL+R。 现在应该显示发行说明", 102 | "nd": "在 \"Install update?\" 单选输入中选择 \"YES\"", 103 | "rd": "按 \"Enter\"" 104 | }, 105 | "then": "termscp x.y.z has been installed!”。 只需重新启动termscp并享受更新😄", 106 | "pex": " 如果您之前已经通过 Deb/RPM 包安装了 termscp,您可能需要使用 CLI 方法通过 sudo 运行 termscp" 107 | }, 108 | "cli": { 109 | "title": "命令行方式", 110 | "body": "如果您愿意,可以仅使用专用 CLI 选项安装新更新:", 111 | "pex": "如有必要,使用 sudo 运行(使用 RPM/DEB 安装)", 112 | "then": "启动后,系统将提示您是否安装更新。 确认安装和 ta-dah,新版本的termscp 现在应该可以在你的机器上使用了" 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /site/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /site/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | https://termscp.veeso.dev/ 6 | 2023-07-05 7 | 1.00 8 | 9 | 10 | https://termscp.veeso.dev/get-started.html 11 | 2023-07-05 12 | 0.95 13 | 14 | 15 | https://termscp.veeso.dev/updates.html 16 | 2023-07-05 17 | 0.80 18 | 19 | 20 | https://termscp.veeso.dev/user-manual.html 21 | 2023-07-05 22 | 0.90 23 | 24 | 25 | https://termscp.veeso.dev/changelog.html 26 | 2023-07-05 27 | 0.50 28 | 29 | -------------------------------------------------------------------------------- /site/updates.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Install termscp updates | termscp 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /site/user-manual.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | termscp user manual | termscp 7 | 8 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 |
47 |
48 |
49 |
50 |
51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | //! ## CLI opts 2 | //! 3 | //! defines the types for main.rs types 4 | 5 | mod remote; 6 | 7 | use std::path::PathBuf; 8 | use std::time::Duration; 9 | 10 | use argh::FromArgs; 11 | pub use remote::{Remote, RemoteArgs}; 12 | 13 | use crate::activity_manager::NextActivity; 14 | use crate::system::logging::LogLevel; 15 | 16 | pub enum Task { 17 | Activity(NextActivity), 18 | ImportTheme(PathBuf), 19 | InstallUpdate, 20 | Version, 21 | } 22 | 23 | #[derive(Default, FromArgs)] 24 | #[argh(description = " 25 | where positional can be: 26 | - [address_a] [address_b] [local-wrkdir] 27 | OR 28 | - -b [bookmark-name_1] -b [bookmark-name_2] [local-wrkdir] 29 | 30 | and any combination of the above 31 | 32 | Address syntax can be: 33 | 34 | - `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp 35 | - `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol 36 | - `\\\\[:port]\\[\\path]` for SMB (on Windows) 37 | - `smb://[user@][:port][/path]` for SMB (on other systems) 38 | 39 | Please, report issues to 40 | Please, consider supporting the author ")] 41 | pub struct Args { 42 | #[argh(subcommand)] 43 | pub nested: Option, 44 | /// resolve address argument as a bookmark name 45 | #[argh(option, short = 'b')] 46 | pub bookmark: Vec, 47 | /// enable TRACE log level 48 | #[argh(switch, short = 'D')] 49 | pub debug: bool, 50 | /// provide password from CLI; if you need to provide multiple passwords, use multiple -P flags. 51 | /// In case just respect the order of the addresses 52 | #[argh(option, short = 'P')] 53 | pub password: Vec, 54 | /// disable logging 55 | #[argh(switch, short = 'q')] 56 | pub quiet: bool, 57 | /// set UI ticks; default 10ms 58 | #[argh(option, short = 'T', default = "10")] 59 | pub ticks: u64, 60 | /// print version 61 | #[argh(switch, short = 'v')] 62 | pub version: bool, 63 | /// disable keyring support 64 | #[argh(switch)] 65 | pub wno_keyring: bool, 66 | // -- positional 67 | #[argh(positional, description = "address1 address2 local-wrkdir")] 68 | pub positional: Vec, 69 | } 70 | 71 | #[derive(FromArgs)] 72 | #[argh(subcommand)] 73 | pub enum ArgsSubcommands { 74 | Config(ConfigArgs), 75 | LoadTheme(LoadThemeArgs), 76 | Update(UpdateArgs), 77 | } 78 | 79 | #[derive(FromArgs)] 80 | /// open termscp configuration 81 | #[argh(subcommand, name = "config")] 82 | pub struct ConfigArgs {} 83 | 84 | #[derive(FromArgs)] 85 | /// update termscp to the latest version 86 | #[argh(subcommand, name = "update")] 87 | pub struct UpdateArgs {} 88 | 89 | #[derive(FromArgs)] 90 | /// import the specified theme 91 | #[argh(subcommand, name = "theme")] 92 | pub struct LoadThemeArgs { 93 | #[argh(positional)] 94 | /// theme file 95 | pub theme: PathBuf, 96 | } 97 | 98 | pub struct RunOpts { 99 | pub remote: RemoteArgs, 100 | pub keyring: bool, 101 | pub ticks: Duration, 102 | pub log_level: LogLevel, 103 | pub task: Task, 104 | } 105 | 106 | impl RunOpts { 107 | pub fn config() -> Self { 108 | Self { 109 | task: Task::Activity(NextActivity::SetupActivity), 110 | ..Default::default() 111 | } 112 | } 113 | 114 | pub fn update() -> Self { 115 | Self { 116 | task: Task::InstallUpdate, 117 | ..Default::default() 118 | } 119 | } 120 | 121 | pub fn import_theme(theme: PathBuf) -> Self { 122 | Self { 123 | task: Task::ImportTheme(theme), 124 | ..Default::default() 125 | } 126 | } 127 | } 128 | 129 | impl Default for RunOpts { 130 | fn default() -> Self { 131 | Self { 132 | remote: RemoteArgs::default(), 133 | ticks: Duration::from_millis(10), 134 | keyring: true, 135 | log_level: LogLevel::Info, 136 | task: Task::Activity(NextActivity::Authentication), 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/config/bookmarks/aws_s3.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::filetransfer::params::AwsS3Params; 4 | 5 | /// Connection parameters for Aws s3 protocol 6 | #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)] 7 | pub struct S3Params { 8 | pub bucket: String, 9 | pub region: Option, 10 | pub endpoint: Option, 11 | pub profile: Option, 12 | pub access_key: Option, 13 | pub secret_access_key: Option, 14 | /// NOTE: there are no session token and security token since they are always temporary 15 | pub new_path_style: Option, 16 | } 17 | 18 | impl From for S3Params { 19 | fn from(params: AwsS3Params) -> Self { 20 | S3Params { 21 | bucket: params.bucket_name, 22 | region: params.region, 23 | endpoint: params.endpoint, 24 | profile: params.profile, 25 | access_key: params.access_key, 26 | secret_access_key: params.secret_access_key, 27 | new_path_style: Some(params.new_path_style), 28 | } 29 | } 30 | } 31 | 32 | impl From for AwsS3Params { 33 | fn from(params: S3Params) -> Self { 34 | AwsS3Params::new(params.bucket, params.region, params.profile) 35 | .endpoint(params.endpoint) 36 | .access_key(params.access_key) 37 | .secret_access_key(params.secret_access_key) 38 | .new_path_style(params.new_path_style.unwrap_or(false)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/config/bookmarks/kube.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::filetransfer::params::KubeProtocolParams; 4 | 5 | /// Extra Connection parameters for Kube protocol 6 | #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)] 7 | pub struct KubeParams { 8 | pub namespace: Option, 9 | pub cluster_url: Option, 10 | pub username: Option, 11 | pub client_cert: Option, 12 | pub client_key: Option, 13 | } 14 | 15 | impl From for KubeProtocolParams { 16 | fn from(value: KubeParams) -> Self { 17 | Self { 18 | namespace: value.namespace, 19 | cluster_url: value.cluster_url, 20 | username: value.username, 21 | client_cert: value.client_cert, 22 | client_key: value.client_key, 23 | } 24 | } 25 | } 26 | 27 | impl From for KubeParams { 28 | fn from(value: KubeProtocolParams) -> Self { 29 | Self { 30 | namespace: value.namespace, 31 | cluster_url: value.cluster_url, 32 | username: value.username, 33 | client_cert: value.client_cert, 34 | client_key: value.client_key, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/config/bookmarks/smb.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::filetransfer::params::SmbParams as TransferSmbParams; 4 | 5 | /// Extra Connection parameters for SMB protocol 6 | #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Default)] 7 | pub struct SmbParams { 8 | pub share: String, 9 | pub workgroup: Option, 10 | } 11 | 12 | #[cfg(posix)] 13 | impl From for SmbParams { 14 | fn from(params: TransferSmbParams) -> Self { 15 | Self { 16 | share: params.share, 17 | workgroup: params.workgroup, 18 | } 19 | } 20 | } 21 | 22 | #[cfg(win)] 23 | impl From for SmbParams { 24 | fn from(params: TransferSmbParams) -> Self { 25 | Self { 26 | share: params.share, 27 | workgroup: None, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Config 2 | //! 3 | //! `config` is the module which provides access to all the termscp configurations 4 | 5 | // export 6 | 7 | pub mod bookmarks; 8 | pub mod params; 9 | pub mod serialization; 10 | pub mod themes; 11 | -------------------------------------------------------------------------------- /src/explorer/builder.rs: -------------------------------------------------------------------------------- 1 | //! ## Builder 2 | //! 3 | //! `builder` is the module which provides a builder for FileExplorer 4 | 5 | // Locals 6 | // Ext 7 | use std::collections::VecDeque; 8 | 9 | use super::formatter::Formatter; 10 | use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs}; 11 | 12 | /// Struct used to create a `FileExplorer` 13 | pub struct FileExplorerBuilder { 14 | explorer: Option, 15 | } 16 | 17 | impl FileExplorerBuilder { 18 | /// Build a new `FileExplorerBuilder` 19 | pub fn new() -> Self { 20 | FileExplorerBuilder { 21 | explorer: Some(FileExplorer::default()), 22 | } 23 | } 24 | 25 | /// Take FileExplorer out of builder 26 | pub fn build(&mut self) -> FileExplorer { 27 | self.explorer.take().unwrap() 28 | } 29 | 30 | /// Enable HIDDEN_FILES option 31 | pub fn with_hidden_files(&mut self, val: bool) -> &mut FileExplorerBuilder { 32 | if let Some(e) = self.explorer.as_mut() { 33 | match val { 34 | true => e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES), 35 | false => e.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES), 36 | } 37 | } 38 | self 39 | } 40 | 41 | /// Set sorting method 42 | pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder { 43 | if let Some(e) = self.explorer.as_mut() { 44 | e.sort_by(sorting); 45 | } 46 | self 47 | } 48 | 49 | /// Enable DIRS_FIRST option 50 | pub fn with_group_dirs(&mut self, group_dirs: Option) -> &mut FileExplorerBuilder { 51 | if let Some(e) = self.explorer.as_mut() { 52 | e.group_dirs_by(group_dirs); 53 | } 54 | self 55 | } 56 | 57 | /// Set stack size for FileExplorer 58 | pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder { 59 | if let Some(e) = self.explorer.as_mut() { 60 | e.stack_size = sz; 61 | e.dirstack = VecDeque::with_capacity(sz); 62 | } 63 | self 64 | } 65 | 66 | /// Set formatter for FileExplorer 67 | pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder { 68 | if let Some(e) = self.explorer.as_mut() { 69 | if let Some(fmt_str) = fmt_str { 70 | e.fmt = Formatter::new(fmt_str); 71 | } 72 | } 73 | self 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | 80 | use pretty_assertions::assert_eq; 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn test_fs_explorer_builder_new_default() { 86 | let explorer: FileExplorer = FileExplorerBuilder::new().build(); 87 | // Verify 88 | assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); 89 | assert_eq!(explorer.file_sorting, FileSorting::Name); // Default 90 | assert_eq!(explorer.group_dirs, None); 91 | assert_eq!(explorer.stack_size, 16); 92 | } 93 | 94 | #[test] 95 | fn test_fs_explorer_builder_new_all() { 96 | let explorer: FileExplorer = FileExplorerBuilder::new() 97 | .with_file_sorting(FileSorting::ModifyTime) 98 | .with_group_dirs(Some(GroupDirs::First)) 99 | .with_hidden_files(true) 100 | .with_stack_size(24) 101 | .with_formatter(Some("{NAME}")) 102 | .build(); 103 | // Verify 104 | assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); 105 | assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default 106 | assert_eq!(explorer.group_dirs, Some(GroupDirs::First)); 107 | assert_eq!(explorer.stack_size, 24); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/filetransfer/host_bridge_builder.rs: -------------------------------------------------------------------------------- 1 | use super::{HostBridgeParams, RemoteFsBuilder}; 2 | use crate::host::{HostBridge, Localhost, RemoteBridged}; 3 | use crate::system::config_client::ConfigClient; 4 | 5 | pub struct HostBridgeBuilder; 6 | 7 | impl HostBridgeBuilder { 8 | /// Build Host Bridge from parms 9 | /// 10 | /// if protocol and parameters are inconsistent, the function will return an error. 11 | pub fn build( 12 | params: HostBridgeParams, 13 | config_client: &ConfigClient, 14 | ) -> Result, String> { 15 | match params { 16 | HostBridgeParams::Localhost(path) => Localhost::new(path) 17 | .map(|host| Box::new(host) as Box) 18 | .map_err(|e| e.to_string()), 19 | HostBridgeParams::Remote(protocol, params) => { 20 | RemoteFsBuilder::build(protocol, params, config_client) 21 | .map(|host| Box::new(RemoteBridged::from(host)) as Box) 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/filetransfer/params/kube.rs: -------------------------------------------------------------------------------- 1 | use remotefs_kube::Config; 2 | 3 | /// Protocol params used by WebDAV 4 | #[derive(Debug, Clone)] 5 | pub struct KubeProtocolParams { 6 | pub namespace: Option, 7 | pub cluster_url: Option, 8 | pub username: Option, 9 | pub client_cert: Option, 10 | pub client_key: Option, 11 | } 12 | 13 | impl KubeProtocolParams { 14 | pub fn set_default_secret(&mut self, _secret: String) {} 15 | 16 | pub fn password_missing(&self) -> bool { 17 | false 18 | } 19 | 20 | pub fn config(self) -> Option { 21 | if let Some(cluster_url) = self.cluster_url { 22 | let mut config = Config::new(cluster_url.parse().unwrap_or_default()); 23 | config.auth_info.username = self.username; 24 | config.auth_info.client_certificate = self.client_cert; 25 | config.auth_info.client_key = self.client_key; 26 | if let Some(namespace) = self.namespace { 27 | config.default_namespace = namespace; 28 | } 29 | 30 | Some(config) 31 | } else { 32 | None 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/filetransfer/params/smb.rs: -------------------------------------------------------------------------------- 1 | /// Connection parameters for SMB protocol 2 | #[derive(Debug, Clone)] 3 | pub struct SmbParams { 4 | pub address: String, 5 | #[cfg(posix)] 6 | pub port: u16, 7 | pub share: String, 8 | pub username: Option, 9 | pub password: Option, 10 | #[cfg(posix)] 11 | pub workgroup: Option, 12 | } 13 | 14 | // -- SMB params 15 | 16 | impl SmbParams { 17 | /// Instantiates a new `AwsS3Params` struct 18 | pub fn new>(address: S, share: S) -> Self { 19 | Self { 20 | address: address.as_ref().to_string(), 21 | #[cfg(posix)] 22 | port: 445, 23 | share: share.as_ref().to_string(), 24 | username: None, 25 | password: None, 26 | #[cfg(posix)] 27 | workgroup: None, 28 | } 29 | } 30 | 31 | #[cfg(posix)] 32 | pub fn port(mut self, port: u16) -> Self { 33 | self.port = port; 34 | self 35 | } 36 | 37 | pub fn username(mut self, username: Option) -> Self { 38 | self.username = username.map(|x| x.to_string()); 39 | self 40 | } 41 | 42 | pub fn password(mut self, password: Option) -> Self { 43 | self.password = password.map(|x| x.to_string()); 44 | self 45 | } 46 | 47 | #[cfg(posix)] 48 | pub fn workgroup(mut self, workgroup: Option) -> Self { 49 | self.workgroup = workgroup.map(|x| x.to_string()); 50 | self 51 | } 52 | 53 | /// Returns whether a password is supposed to be required for this protocol params. 54 | /// The result true is returned ONLY if the supposed secret is MISSING!!! 55 | pub fn password_missing(&self) -> bool { 56 | self.password.is_none() 57 | } 58 | 59 | /// Set password 60 | #[cfg(posix)] 61 | pub fn set_default_secret(&mut self, secret: String) { 62 | self.password = Some(secret); 63 | } 64 | 65 | #[cfg(win)] 66 | pub fn set_default_secret(&mut self, _secret: String) {} 67 | } 68 | 69 | #[cfg(test)] 70 | mod test { 71 | 72 | use pretty_assertions::assert_eq; 73 | 74 | use super::*; 75 | 76 | #[test] 77 | fn should_init_smb_params() { 78 | let params = SmbParams::new("localhost", "temp"); 79 | assert_eq!(¶ms.address, "localhost"); 80 | 81 | #[cfg(posix)] 82 | assert_eq!(params.port, 445); 83 | assert_eq!(¶ms.share, "temp"); 84 | 85 | #[cfg(posix)] 86 | assert!(params.username.is_none()); 87 | #[cfg(posix)] 88 | assert!(params.password.is_none()); 89 | #[cfg(posix)] 90 | assert!(params.workgroup.is_none()); 91 | } 92 | 93 | #[test] 94 | #[cfg(posix)] 95 | fn should_init_smb_params_with_optionals() { 96 | let params = SmbParams::new("localhost", "temp") 97 | .port(3456) 98 | .username(Some("foo")) 99 | .password(Some("bar")) 100 | .workgroup(Some("baz")); 101 | 102 | assert_eq!(¶ms.address, "localhost"); 103 | assert_eq!(params.port, 3456); 104 | assert_eq!(¶ms.share, "temp"); 105 | assert_eq!(params.username.as_deref().unwrap(), "foo"); 106 | assert_eq!(params.password.as_deref().unwrap(), "bar"); 107 | assert_eq!(params.workgroup.as_deref().unwrap(), "baz"); 108 | } 109 | 110 | #[test] 111 | #[cfg(win)] 112 | fn should_init_smb_params_with_optionals() { 113 | let params = SmbParams::new("localhost", "temp") 114 | .username(Some("foo")) 115 | .password(Some("bar")); 116 | 117 | assert_eq!(¶ms.address, "localhost"); 118 | assert_eq!(¶ms.share, "temp"); 119 | assert_eq!(params.username.as_deref().unwrap(), "foo"); 120 | assert_eq!(params.password.as_deref().unwrap(), "bar"); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/filetransfer/params/webdav.rs: -------------------------------------------------------------------------------- 1 | /// Protocol params used by WebDAV 2 | #[derive(Debug, Clone)] 3 | pub struct WebDAVProtocolParams { 4 | pub uri: String, 5 | pub username: String, 6 | pub password: String, 7 | } 8 | 9 | impl WebDAVProtocolParams { 10 | pub fn set_default_secret(&mut self, secret: String) { 11 | self.password = secret; 12 | } 13 | 14 | pub fn password_missing(&self) -> bool { 15 | self.password.is_empty() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/host/bridge.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use remotefs::File; 5 | use remotefs::fs::{Metadata, UnixPex}; 6 | 7 | use super::HostResult; 8 | 9 | /// Trait to bridge a remote filesystem to the host filesystem 10 | /// 11 | /// In case of `Localhost` this should be effortless, while for remote hosts this should 12 | /// implement a real bridge when the resource is first loaded on the local 13 | /// filesystem and then processed on the remote. 14 | pub trait HostBridge { 15 | /// Connect to host 16 | fn connect(&mut self) -> HostResult<()>; 17 | 18 | /// Disconnect from host 19 | fn disconnect(&mut self) -> HostResult<()>; 20 | 21 | /// Returns whether the host is connected 22 | fn is_connected(&mut self) -> bool; 23 | 24 | /// Returns whether the host is localhost 25 | fn is_localhost(&self) -> bool; 26 | 27 | /// Print working directory 28 | fn pwd(&mut self) -> HostResult; 29 | 30 | /// Change working directory with the new provided directory 31 | fn change_wrkdir(&mut self, new_dir: &Path) -> HostResult; 32 | 33 | /// Make a directory at path and update the file list (only if relative) 34 | fn mkdir(&mut self, dir_name: &Path) -> HostResult<()> { 35 | self.mkdir_ex(dir_name, false) 36 | } 37 | 38 | /// Extended option version of makedir. 39 | /// ignex: don't report error if directory already exists 40 | fn mkdir_ex(&mut self, dir_name: &Path, ignore_existing: bool) -> HostResult<()>; 41 | 42 | /// Remove file entry 43 | fn remove(&mut self, entry: &File) -> HostResult<()>; 44 | 45 | /// Rename file or directory to new name 46 | fn rename(&mut self, entry: &File, dst_path: &Path) -> HostResult<()>; 47 | 48 | /// Copy file to destination path 49 | fn copy(&mut self, entry: &File, dst: &Path) -> HostResult<()>; 50 | 51 | /// Stat file and create a File 52 | fn stat(&mut self, path: &Path) -> HostResult; 53 | 54 | /// Returns whether provided file path exists 55 | fn exists(&mut self, path: &Path) -> HostResult; 56 | 57 | /// Get content of a directory 58 | fn list_dir(&mut self, path: &Path) -> HostResult>; 59 | 60 | /// Set file stat 61 | fn setstat(&mut self, path: &Path, metadata: &Metadata) -> HostResult<()>; 62 | 63 | /// Execute a command on localhost 64 | fn exec(&mut self, cmd: &str) -> HostResult; 65 | 66 | /// Create a symlink from src to dst 67 | fn symlink(&mut self, src: &Path, dst: &Path) -> HostResult<()>; 68 | 69 | /// Change file mode to file, according to UNIX permissions 70 | fn chmod(&mut self, path: &Path, pex: UnixPex) -> HostResult<()>; 71 | 72 | /// Open file for reading 73 | fn open_file(&mut self, file: &Path) -> HostResult>; 74 | 75 | /// Open file for writing 76 | fn create_file( 77 | &mut self, 78 | file: &Path, 79 | metadata: &Metadata, 80 | ) -> HostResult>; 81 | 82 | /// Finalize write operation 83 | fn finalize_write(&mut self, writer: Box) -> HostResult<()>; 84 | } 85 | -------------------------------------------------------------------------------- /src/host/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Host 2 | //! 3 | //! `host` is the module which provides functionalities to host file system 4 | 5 | mod bridge; 6 | mod localhost; 7 | mod remote_bridged; 8 | 9 | use std::path::{Path, PathBuf}; 10 | 11 | use thiserror::Error; 12 | 13 | // Locals 14 | pub use self::bridge::HostBridge; 15 | pub use self::localhost::Localhost; 16 | pub use self::remote_bridged::RemoteBridged; 17 | 18 | pub type HostResult = Result; 19 | 20 | /// HostErrorType provides an overview of the specific host error 21 | #[derive(Error, Debug)] 22 | #[allow(dead_code)] 23 | pub enum HostErrorType { 24 | #[error("No such file or directory")] 25 | NoSuchFileOrDirectory, 26 | #[error("File is readonly")] 27 | ReadonlyFile, 28 | #[error("Could not access directory")] 29 | DirNotAccessible, 30 | #[error("Could not access file")] 31 | FileNotAccessible, 32 | #[error("File already exists")] 33 | FileAlreadyExists, 34 | #[error("Could not create file")] 35 | CouldNotCreateFile, 36 | #[error("Command execution failed")] 37 | ExecutionFailed, 38 | #[error("Could not delete file")] 39 | DeleteFailed, 40 | #[error("Not implemented")] 41 | NotImplemented, 42 | #[error("remote fs error: {0}")] 43 | RemoteFs(#[from] remotefs::RemoteError), 44 | } 45 | 46 | /// HostError is a wrapper for the error type and the exact io error 47 | #[derive(Debug, Error)] 48 | pub struct HostError { 49 | pub error: HostErrorType, 50 | ioerr: Option, 51 | path: Option, 52 | } 53 | 54 | impl From for HostError { 55 | fn from(value: remotefs::RemoteError) -> Self { 56 | HostError::from(HostErrorType::RemoteFs(value)) 57 | } 58 | } 59 | 60 | impl HostError { 61 | /// Instantiates a new HostError 62 | pub(crate) fn new(error: HostErrorType, errno: Option, p: &Path) -> Self { 63 | HostError { 64 | error, 65 | ioerr: errno, 66 | path: Some(p.to_path_buf()), 67 | } 68 | } 69 | } 70 | 71 | impl From for HostError { 72 | fn from(error: HostErrorType) -> Self { 73 | HostError { 74 | error, 75 | ioerr: None, 76 | path: None, 77 | } 78 | } 79 | } 80 | 81 | impl std::fmt::Display for HostError { 82 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 83 | let p_str: String = match self.path.as_ref() { 84 | None => String::new(), 85 | Some(p) => format!(" ({})", p.display()), 86 | }; 87 | match &self.ioerr { 88 | Some(err) => write!(f, "{}: {}{}", self.error, err, p_str), 89 | None => write!(f, "{}{}", self.error, p_str), 90 | } 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod test { 96 | 97 | use pretty_assertions::assert_eq; 98 | 99 | use super::*; 100 | 101 | #[test] 102 | fn test_host_fmt_error() { 103 | let err: HostError = HostError::new( 104 | HostErrorType::CouldNotCreateFile, 105 | Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)), 106 | Path::new("/tmp"), 107 | ); 108 | assert_eq!( 109 | format!("{err}"), 110 | String::from("Could not create file: address in use (/tmp)"), 111 | ); 112 | assert_eq!( 113 | format!("{}", HostError::from(HostErrorType::DeleteFailed)), 114 | String::from("Could not delete file") 115 | ); 116 | assert_eq!( 117 | format!("{}", HostError::from(HostErrorType::ExecutionFailed)), 118 | String::from("Command execution failed"), 119 | ); 120 | assert_eq!( 121 | format!("{}", HostError::from(HostErrorType::DirNotAccessible)), 122 | String::from("Could not access directory"), 123 | ); 124 | assert_eq!( 125 | format!("{}", HostError::from(HostErrorType::NoSuchFileOrDirectory)), 126 | String::from("No such file or directory") 127 | ); 128 | assert_eq!( 129 | format!("{}", HostError::from(HostErrorType::ReadonlyFile)), 130 | String::from("File is readonly") 131 | ); 132 | assert_eq!( 133 | format!("{}", HostError::from(HostErrorType::FileNotAccessible)), 134 | String::from("Could not access file") 135 | ); 136 | assert_eq!( 137 | format!("{}", HostError::from(HostErrorType::FileAlreadyExists)), 138 | String::from("File already exists") 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/host/remote_bridged/temp_mapped_file.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, Read, Write}; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | use tempfile::NamedTempFile; 6 | 7 | use crate::host::{HostError, HostErrorType, HostResult}; 8 | 9 | /// A temporary file mapped to a remote file which has been transferred to local 10 | /// and which supports read/write operations 11 | #[derive(Debug, Clone)] 12 | pub struct TempMappedFile { 13 | tempfile: Arc, 14 | handle: Arc>>, 15 | } 16 | 17 | impl Write for TempMappedFile { 18 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 19 | let rc = self.write_hnd()?; 20 | let mut ref_mut = rc.lock().unwrap(); 21 | ref_mut.as_mut().unwrap().write(buf) 22 | } 23 | 24 | fn flush(&mut self) -> std::io::Result<()> { 25 | let rc = self.write_hnd()?; 26 | let mut ref_mut = rc.lock().unwrap(); 27 | ref_mut.as_mut().unwrap().flush() 28 | } 29 | } 30 | 31 | impl Read for TempMappedFile { 32 | fn read(&mut self, buf: &mut [u8]) -> std::io::Result { 33 | let rc = self.read_hnd()?; 34 | let mut ref_mut = rc.lock().unwrap(); 35 | ref_mut.as_mut().unwrap().read(buf) 36 | } 37 | } 38 | 39 | impl TempMappedFile { 40 | pub fn new() -> HostResult { 41 | NamedTempFile::new() 42 | .map(|tempfile| TempMappedFile { 43 | tempfile: Arc::new(tempfile), 44 | handle: Arc::new(Mutex::new(None)), 45 | }) 46 | .map_err(|e| { 47 | HostError::new( 48 | HostErrorType::CouldNotCreateFile, 49 | Some(e), 50 | std::path::Path::new(""), 51 | ) 52 | }) 53 | } 54 | 55 | /// Syncs the file to disk and frees the file handle. 56 | /// 57 | /// Must be called 58 | pub fn sync(&mut self) -> HostResult<()> { 59 | { 60 | let mut lock = self.handle.lock().unwrap(); 61 | 62 | if let Some(hnd) = lock.take() { 63 | hnd.sync_all().map_err(|e| { 64 | HostError::new( 65 | HostErrorType::FileNotAccessible, 66 | Some(e), 67 | self.tempfile.path(), 68 | ) 69 | })?; 70 | } 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | fn write_hnd(&mut self) -> io::Result>>> { 77 | { 78 | let mut lock = self.handle.lock().unwrap(); 79 | if lock.is_none() { 80 | let hnd = File::create(self.tempfile.path())?; 81 | lock.replace(hnd); 82 | } 83 | } 84 | 85 | Ok(self.handle.clone()) 86 | } 87 | 88 | fn read_hnd(&mut self) -> io::Result>>> { 89 | { 90 | let mut lock = self.handle.lock().unwrap(); 91 | if lock.is_none() { 92 | let hnd = File::open(self.tempfile.path())?; 93 | lock.replace(hnd); 94 | } 95 | } 96 | 97 | Ok(self.handle.clone()) 98 | } 99 | } 100 | 101 | #[cfg(test)] 102 | mod test { 103 | 104 | use pretty_assertions::assert_eq; 105 | 106 | use super::*; 107 | 108 | #[test] 109 | fn test_should_write_and_read_file() { 110 | let mut file = TempMappedFile::new().unwrap(); 111 | file.write_all(b"Hello, World!").unwrap(); 112 | 113 | file.sync().unwrap(); 114 | 115 | let mut buf = Vec::new(); 116 | file.read_to_end(&mut buf).unwrap(); 117 | 118 | assert_eq!(buf, b"Hello, World!"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/support.rs: -------------------------------------------------------------------------------- 1 | //! ## Support 2 | //! 3 | //! this module exposes some extra run modes for termscp, meant to be used for "support", such as installing themes 4 | 5 | // mod 6 | use std::fs; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use crate::system::auto_update::{Update, UpdateStatus}; 10 | use crate::system::config_client::ConfigClient; 11 | use crate::system::environment; 12 | use crate::system::notifications::Notification; 13 | use crate::system::theme_provider::ThemeProvider; 14 | 15 | /// Import theme at provided path into termscp 16 | pub fn import_theme(p: &Path) -> Result<(), String> { 17 | if !p.exists() { 18 | return Err(String::from( 19 | "Could not import theme: No such file or directory", 20 | )); 21 | } 22 | // Validate theme file 23 | ThemeProvider::new(p).map_err(|e| format!("Invalid theme error: {e}"))?; 24 | // get config dir 25 | let cfg_dir: PathBuf = get_config_dir()?; 26 | // Get theme directory 27 | let theme_file: PathBuf = environment::get_theme_path(cfg_dir.as_path()); 28 | // Copy theme to theme_dir 29 | fs::copy(p, theme_file.as_path()) 30 | .map(|_| ()) 31 | .map_err(|e| format!("Could not import theme: {e}")) 32 | } 33 | 34 | /// Install latest version of termscp if an update is available 35 | pub fn install_update() -> Result { 36 | match Update::default() 37 | .show_progress(true) 38 | .ask_confirm(true) 39 | .upgrade() 40 | { 41 | Ok(UpdateStatus::AlreadyUptodate) => Ok("termscp is already up to date".to_string()), 42 | Ok(UpdateStatus::UpdateInstalled(v)) => { 43 | if get_config_client() 44 | .map(|x| x.get_notifications()) 45 | .unwrap_or(true) 46 | { 47 | Notification::update_installed(v.as_str()); 48 | } 49 | Ok(format!("termscp has been updated to version {v}")) 50 | } 51 | Err(err) => { 52 | if get_config_client() 53 | .map(|x| x.get_notifications()) 54 | .unwrap_or(true) 55 | { 56 | Notification::update_failed(err.to_string()); 57 | } 58 | Err(err.to_string()) 59 | } 60 | } 61 | } 62 | 63 | /// Get configuration directory 64 | fn get_config_dir() -> Result { 65 | match environment::init_config_dir() { 66 | Ok(Some(config_dir)) => Ok(config_dir), 67 | Ok(None) => Err(String::from( 68 | "Your system doesn't provide a configuration directory", 69 | )), 70 | Err(err) => Err(format!( 71 | "Could not initialize configuration directory: {err}" 72 | )), 73 | } 74 | } 75 | 76 | /// Get configuration client 77 | fn get_config_client() -> Option { 78 | match get_config_dir() { 79 | Err(_) => None, 80 | Ok(dir) => { 81 | let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path()); 82 | match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) { 83 | Err(_) => None, 84 | Ok(c) => Some(c), 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/system/keys/filestorage.rs: -------------------------------------------------------------------------------- 1 | //! ## FileStorage 2 | //! 3 | //! `filestorage` provides an implementation of the `KeyStorage` trait using a file 4 | 5 | // Local 6 | // Ext 7 | use std::fs::{OpenOptions, Permissions}; 8 | use std::io::{Read, Write}; 9 | use std::path::{Path, PathBuf}; 10 | 11 | use super::{KeyStorage, KeyStorageError}; 12 | 13 | /// File storage is an implementation o the `KeyStorage` which uses a file to store the key 14 | pub struct FileStorage { 15 | dir_path: PathBuf, 16 | } 17 | 18 | impl FileStorage { 19 | /// Instantiates a new `FileStorage` 20 | pub fn new(dir_path: &Path) -> Self { 21 | FileStorage { 22 | dir_path: PathBuf::from(dir_path), 23 | } 24 | } 25 | 26 | /// Make file path for key file from `dir_path` and the application id 27 | fn make_file_path(&self, storage_id: &str) -> PathBuf { 28 | let mut p: PathBuf = self.dir_path.clone(); 29 | let file_name = format!(".{storage_id}.key"); 30 | p.push(file_name); 31 | p 32 | } 33 | } 34 | 35 | impl KeyStorage for FileStorage { 36 | /// Retrieve key from the key storage. 37 | /// The key might be acccess through an identifier, which identifies 38 | /// the key in the storage 39 | fn get_key(&self, storage_id: &str) -> Result { 40 | let key_file: PathBuf = self.make_file_path(storage_id); 41 | // Check if file exists 42 | if !key_file.exists() { 43 | return Err(KeyStorageError::NoSuchKey); 44 | } 45 | // Read key from file 46 | match OpenOptions::new().read(true).open(key_file.as_path()) { 47 | Ok(mut file) => { 48 | let mut key: String = String::new(); 49 | match file.read_to_string(&mut key) { 50 | Ok(_) => Ok(key), 51 | Err(_) => Err(KeyStorageError::ProviderError), 52 | } 53 | } 54 | Err(_) => Err(KeyStorageError::ProviderError), 55 | } 56 | } 57 | 58 | /// Set the key into the key storage 59 | fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { 60 | let key_file: PathBuf = self.make_file_path(storage_id); 61 | // Write key 62 | match OpenOptions::new() 63 | .write(true) 64 | .create(true) 65 | .truncate(true) 66 | .open(key_file.as_path()) 67 | { 68 | Ok(mut file) => { 69 | // Write key to file 70 | if file.write_all(key.as_bytes()).is_err() { 71 | return Err(KeyStorageError::ProviderError); 72 | } 73 | // Set file to readonly 74 | let mut permissions: Permissions = file.metadata().unwrap().permissions(); 75 | permissions.set_readonly(true); 76 | let _ = file.set_permissions(permissions); 77 | Ok(()) 78 | } 79 | Err(_) => Err(KeyStorageError::ProviderError), 80 | } 81 | } 82 | 83 | /// is_supported 84 | /// 85 | /// Returns whether the key storage is supported on the host system 86 | fn is_supported(&self) -> bool { 87 | true 88 | } 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | 94 | use pretty_assertions::assert_eq; 95 | 96 | use super::*; 97 | 98 | #[test] 99 | fn test_system_keys_filestorage_make_dir() { 100 | let storage: FileStorage = FileStorage::new(Path::new("/tmp/")); 101 | assert_eq!( 102 | storage.make_file_path("bookmarks").as_path(), 103 | Path::new("/tmp/.bookmarks.key") 104 | ); 105 | } 106 | 107 | #[test] 108 | fn test_system_keys_filestorage_ok() { 109 | let key_dir: tempfile::TempDir = 110 | tempfile::TempDir::new().expect("Could not create tempdir"); 111 | let storage: FileStorage = FileStorage::new(key_dir.path()); 112 | // Supported 113 | assert!(storage.is_supported()); 114 | let app_name: &str = "termscp"; 115 | let secret: &str = "Th15-15/My-Супер-Секрет"; 116 | // Secret should not exist 117 | assert!(storage.get_key(app_name).is_err()); 118 | // Write secret 119 | assert!(storage.set_key(app_name, secret).is_ok()); 120 | // Get secret 121 | assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret); 122 | } 123 | 124 | #[test] 125 | fn test_system_keys_filestorage_err() { 126 | let bad_dir: &Path = Path::new("/piro/poro/pero/"); 127 | let storage: FileStorage = FileStorage::new(bad_dir); 128 | let app_name: &str = "termscp"; 129 | let secret: &str = "Th15-15/My-Супер-Секрет"; 130 | assert!(storage.set_key(app_name, secret).is_err()); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/system/keys/keyringstorage.rs: -------------------------------------------------------------------------------- 1 | //! ## KeyringStorage 2 | //! 3 | //! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring 4 | 5 | // Local 6 | // Ext 7 | use keyring::{Entry as Keyring, Error as KeyringError}; 8 | 9 | use super::{KeyStorage, KeyStorageError}; 10 | 11 | /// provides a `KeyStorage` implementation using the keyring crate 12 | pub struct KeyringStorage { 13 | username: String, 14 | } 15 | 16 | impl KeyringStorage { 17 | /// Instantiates a new KeyringStorage 18 | pub fn new(username: &str) -> Self { 19 | KeyringStorage { 20 | username: username.to_string(), 21 | } 22 | } 23 | } 24 | 25 | impl KeyStorage for KeyringStorage { 26 | /// Retrieve key from the key storage. 27 | /// The key might be acccess through an identifier, which identifies 28 | /// the key in the storage 29 | fn get_key(&self, storage_id: &str) -> Result { 30 | let storage: Keyring = Keyring::new(storage_id, self.username.as_str())?; 31 | match storage.get_password() { 32 | Ok(s) => Ok(s), 33 | Err(e) => match e { 34 | KeyringError::NoEntry => Err(KeyStorageError::NoSuchKey), 35 | KeyringError::PlatformFailure(_) 36 | | KeyringError::NoStorageAccess(_) 37 | | KeyringError::Invalid(_, _) 38 | | KeyringError::Ambiguous(_) => Err(KeyStorageError::ProviderError), 39 | KeyringError::BadEncoding(_) | KeyringError::TooLong(_, _) => { 40 | Err(KeyStorageError::BadSytax) 41 | } 42 | _ => Err(KeyStorageError::ProviderError), 43 | }, 44 | } 45 | } 46 | 47 | /// Set the key into the key storage 48 | fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> { 49 | let storage: Keyring = Keyring::new(storage_id, self.username.as_str())?; 50 | match storage.set_password(key) { 51 | Ok(_) => Ok(()), 52 | Err(_) => Err(KeyStorageError::ProviderError), 53 | } 54 | } 55 | 56 | /// is_supported 57 | /// 58 | /// Returns whether the key storage is supported on the host system 59 | fn is_supported(&self) -> bool { 60 | let dummy: String = String::from("dummy-service"); 61 | let storage: Keyring = match Keyring::new(dummy.as_str(), self.username.as_str()) { 62 | Ok(s) => s, 63 | Err(e) => { 64 | error!("could not instantiate keyring {e}"); 65 | return false; 66 | } 67 | }; 68 | // Check what kind of error is returned 69 | match storage.get_password() { 70 | Ok(_) => true, 71 | Err(KeyringError::NoStorageAccess(_) | KeyringError::PlatformFailure(_)) => false, 72 | Err(_) => true, 73 | } 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | #[test] 80 | #[cfg(all(not(feature = "github-actions"), not(feature = "isolated-tests")))] 81 | fn test_system_keys_keyringstorage() { 82 | use pretty_assertions::assert_eq; 83 | use whoami::username; 84 | 85 | use super::*; 86 | 87 | let username: String = username(); 88 | let storage: KeyringStorage = KeyringStorage::new(username.as_str()); 89 | assert!(storage.is_supported()); 90 | let app_name: &str = "termscp-test2"; 91 | let secret: &str = "Th15-15/My-Супер-Секрет"; 92 | let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap(); 93 | let _ = kring.delete_credential(); 94 | drop(kring); 95 | // Secret should not exist 96 | assert!(storage.get_key(app_name).is_err()); 97 | // Write secret 98 | assert!(storage.set_key(app_name, secret).is_ok()); 99 | // Get secret 100 | assert_eq!(storage.get_key(app_name).unwrap().as_str(), secret); 101 | 102 | // Delete the key manually... 103 | let kring: Keyring = Keyring::new(app_name, username.as_str()).unwrap(); 104 | assert!(kring.delete_credential().is_ok()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/system/keys/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## KeyStorage 2 | //! 3 | //! `keystorage` provides the trait to manipulate to a KeyStorage 4 | 5 | // Storages 6 | pub mod filestorage; 7 | pub mod keyringstorage; 8 | // ext 9 | use keyring::Error as KeyringError; 10 | use thiserror::Error; 11 | 12 | /// defines the error type for the `KeyStorage` 13 | #[derive(Debug, Error)] 14 | pub enum KeyStorageError { 15 | #[error("Key has a bad syntax")] 16 | BadSytax, 17 | #[error("Provider service error")] 18 | ProviderError, 19 | #[error("No such key")] 20 | NoSuchKey, 21 | #[error("keyring error: {0}")] 22 | KeyringError(KeyringError), 23 | } 24 | 25 | impl From for KeyStorageError { 26 | fn from(e: KeyringError) -> Self { 27 | Self::KeyringError(e) 28 | } 29 | } 30 | 31 | /// this traits provides the methods to communicate and interact with the key storage. 32 | pub trait KeyStorage { 33 | /// Retrieve key from the key storage. 34 | /// The key might be acccess through an identifier, which identifies 35 | /// the key in the storage 36 | fn get_key(&self, storage_id: &str) -> Result; 37 | 38 | /// Set the key into the key storage 39 | fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>; 40 | 41 | /// is_supported 42 | /// 43 | /// Returns whether the key storage is supported on the host system 44 | fn is_supported(&self) -> bool; 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | 50 | use pretty_assertions::assert_eq; 51 | 52 | use super::*; 53 | 54 | #[test] 55 | fn test_system_keys_mod_errors() { 56 | assert_eq!( 57 | KeyStorageError::BadSytax.to_string(), 58 | String::from("Key has a bad syntax") 59 | ); 60 | assert_eq!( 61 | KeyStorageError::ProviderError.to_string(), 62 | String::from("Provider service error") 63 | ); 64 | assert_eq!( 65 | KeyStorageError::NoSuchKey.to_string(), 66 | String::from("No such key") 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/system/logging.rs: -------------------------------------------------------------------------------- 1 | //! ## Logging 2 | //! 3 | //! `logging` is the module which initializes the logging system for termscp 4 | 5 | pub use simplelog::LevelFilter as LogLevel; 6 | use simplelog::{ConfigBuilder, WriteLogger}; 7 | 8 | use super::environment::{get_log_paths, init_cache_dir}; 9 | use crate::utils::file::open_file; 10 | 11 | /// Initialize logger 12 | pub fn init(level: LogLevel) -> Result<(), String> { 13 | // Init cache dir 14 | let cache_dir = match init_cache_dir() { 15 | Ok(Some(p)) => p, 16 | Ok(None) => { 17 | return Err(String::from( 18 | "This system doesn't seem to support CACHE_DIR", 19 | )); 20 | } 21 | Err(err) => return Err(err), 22 | }; 23 | let log_file_path = get_log_paths(cache_dir.as_path()); 24 | // Open log file 25 | let file = open_file(log_file_path.as_path(), true, true, false) 26 | .map_err(|e| format!("Failed to open file {}: {}", log_file_path.display(), e))?; 27 | // Prepare log config 28 | let config = ConfigBuilder::new() 29 | .set_time_format_rfc3339() 30 | .add_filter_allow_str("termscp") 31 | .add_filter_allow_str("remotefs") 32 | .add_filter_allow_str("kube") 33 | .add_filter_allow_str("suppaftp") 34 | .add_filter_allow_str("pavao") 35 | .build(); 36 | // Make logger 37 | WriteLogger::init(level, config, file).map_err(|e| format!("Failed to initialize logger: {e}")) 38 | } 39 | 40 | #[cfg(test)] 41 | mod test { 42 | 43 | use super::*; 44 | 45 | #[test] 46 | fn test_system_logging_setup() { 47 | assert!(init(LogLevel::Trace).is_ok()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/system/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## System 2 | //! 3 | //! `system` is the module which contains functions and data types related to current system 4 | 5 | // modules 6 | pub mod auto_update; 7 | pub mod bookmarks_client; 8 | pub mod config_client; 9 | pub mod environment; 10 | mod keys; 11 | pub mod logging; 12 | pub mod notifications; 13 | pub mod sshkey_storage; 14 | pub mod theme_provider; 15 | pub mod watcher; 16 | -------------------------------------------------------------------------------- /src/system/notifications.rs: -------------------------------------------------------------------------------- 1 | //! # Notifications 2 | //! 3 | //! This module exposes the function to send notifications to the guest OS 4 | 5 | #[cfg(all(unix, not(target_os = "macos")))] 6 | use notify_rust::Hint; 7 | use notify_rust::{Notification as OsNotification, Timeout}; 8 | 9 | /// A notification helper which provides all the functions to send the available notifications for termscp 10 | pub struct Notification; 11 | 12 | impl Notification { 13 | /// Notify a transfer has been completed with success 14 | pub fn transfer_completed>(body: S) { 15 | Self::notify( 16 | "Transfer completed ✅", 17 | body.as_ref(), 18 | Some("transfer.complete"), 19 | ); 20 | } 21 | 22 | /// Notify a transfer has failed 23 | pub fn transfer_error>(body: S) { 24 | Self::notify("Transfer failed ❌", body.as_ref(), Some("transfer.error")); 25 | } 26 | 27 | /// Notify a new version of termscp is available for download 28 | pub fn update_available>(version: S) { 29 | Self::notify( 30 | "New version available ⬇️", 31 | format!("termscp {} is now available for download", version.as_ref()).as_str(), 32 | None, 33 | ); 34 | } 35 | 36 | /// Notify the update has been correctly installed 37 | pub fn update_installed>(version: S) { 38 | Self::notify( 39 | "Update installed 🎉", 40 | format!("termscp {} has been installed! Restart termscp to enjoy the latest version of termscp 🙂", version.as_ref()).as_str(), 41 | None, 42 | ); 43 | } 44 | 45 | /// Notify the update installation has failed 46 | pub fn update_failed>(err: S) { 47 | Self::notify("Update installation failed ❌", err.as_ref(), None); 48 | } 49 | 50 | /// Notify guest OS with provided Summary, body and optional category 51 | /// e.g. Category is supported on FreeBSD/Linux only 52 | #[allow(unused_variables)] 53 | fn notify(summary: &str, body: &str, category: Option<&str>) { 54 | let mut notification = OsNotification::new(); 55 | // Set common params 56 | notification 57 | .appname(env!("CARGO_PKG_NAME")) 58 | .summary(summary) 59 | .body(body) 60 | .timeout(Timeout::Milliseconds(10000)); 61 | // Set category if any 62 | #[cfg(all(unix, not(target_os = "macos")))] 63 | if let Some(category) = category { 64 | notification.hint(Hint::Category(category.to_string())); 65 | } 66 | let _ = notification.show(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ui/activities/auth/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Components 2 | //! 3 | //! auth activity components 4 | 5 | use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; 6 | 7 | mod bookmarks; 8 | mod form; 9 | mod popup; 10 | mod text; 11 | 12 | pub use bookmarks::{ 13 | BookmarkName, BookmarkSavePassword, BookmarksList, DeleteBookmarkPopup, DeleteRecentPopup, 14 | RecentsList, 15 | }; 16 | #[cfg(posix)] 17 | pub use form::InputSmbWorkgroup; 18 | pub use form::{ 19 | HostBridgeProtocolRadio, InputAddress, InputKubeClientCert, InputKubeClientKey, 20 | InputKubeClusterUrl, InputKubeNamespace, InputKubeUsername, InputLocalDirectory, InputPassword, 21 | InputPort, InputRemoteDirectory, InputS3AccessKey, InputS3Bucket, InputS3Endpoint, 22 | InputS3Profile, InputS3Region, InputS3SecretAccessKey, InputS3SecurityToken, 23 | InputS3SessionToken, InputSmbShare, InputUsername, InputWebDAVUri, RadioS3NewPathStyle, 24 | RemoteProtocolRadio, 25 | }; 26 | pub use popup::{ 27 | ErrorPopup, InfoPopup, InstallUpdatePopup, Keybindings, QuitPopup, ReleaseNotes, WaitPopup, 28 | WindowSizeError, 29 | }; 30 | pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title}; 31 | use tui_realm_stdlib::Phantom; 32 | use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; 33 | use tuirealm::{Component, MockComponent}; 34 | 35 | // -- global listener 36 | 37 | #[derive(Default, MockComponent)] 38 | pub struct GlobalListener { 39 | component: Phantom, 40 | } 41 | 42 | impl Component for GlobalListener { 43 | fn on(&mut self, ev: Event) -> Option { 44 | match ev { 45 | Event::Keyboard(KeyEvent { 46 | code: Key::Esc | Key::Function(10), 47 | .. 48 | }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), 49 | Event::Keyboard(KeyEvent { 50 | code: Key::Char('c'), 51 | modifiers: KeyModifiers::CONTROL, 52 | }) => Some(Msg::Form(FormMsg::EnterSetup)), 53 | Event::Keyboard(KeyEvent { 54 | code: Key::Char('h'), 55 | modifiers: KeyModifiers::CONTROL, 56 | }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), 57 | Event::Keyboard(KeyEvent { 58 | code: Key::Function(1), 59 | .. 60 | }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), 61 | Event::Keyboard(KeyEvent { 62 | code: Key::Char('r'), 63 | modifiers: KeyModifiers::CONTROL, 64 | }) => Some(Msg::Ui(UiMsg::ShowReleaseNotes)), 65 | Event::Keyboard(KeyEvent { 66 | code: Key::Char('s'), 67 | modifiers: KeyModifiers::CONTROL, 68 | }) => Some(Msg::Ui(UiMsg::ShowSaveBookmarkPopup)), 69 | Event::WindowResize(_, _) => Some(Msg::Ui(UiMsg::WindowResized)), 70 | _ => None, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ui/activities/auth/components/text.rs: -------------------------------------------------------------------------------- 1 | //! ## Text 2 | //! 3 | //! auth activity texts 4 | 5 | use tui_realm_stdlib::{Label, Span}; 6 | use tuirealm::props::{Color, TextModifiers, TextSpan}; 7 | use tuirealm::{Component, Event, MockComponent, NoUserEvent}; 8 | 9 | use super::Msg; 10 | 11 | // -- Title 12 | 13 | #[derive(MockComponent)] 14 | pub struct Title { 15 | component: Label, 16 | } 17 | 18 | impl Default for Title { 19 | fn default() -> Self { 20 | Self { 21 | component: Label::default() 22 | .modifiers(TextModifiers::BOLD | TextModifiers::ITALIC) 23 | .text("$ termscp"), 24 | } 25 | } 26 | } 27 | 28 | impl Component for Title { 29 | fn on(&mut self, _ev: Event) -> Option { 30 | None 31 | } 32 | } 33 | 34 | // -- subtitle 35 | 36 | #[derive(MockComponent)] 37 | pub struct Subtitle { 38 | component: Label, 39 | } 40 | 41 | impl Default for Subtitle { 42 | fn default() -> Self { 43 | Self { 44 | component: Label::default() 45 | .modifiers(TextModifiers::BOLD | TextModifiers::ITALIC) 46 | .text(format!("$ version {}", env!("CARGO_PKG_VERSION"))), 47 | } 48 | } 49 | } 50 | 51 | impl Component for Subtitle { 52 | fn on(&mut self, _ev: Event) -> Option { 53 | None 54 | } 55 | } 56 | 57 | // -- new version disclaimer 58 | 59 | #[derive(MockComponent)] 60 | pub struct NewVersionDisclaimer { 61 | component: Span, 62 | } 63 | 64 | impl NewVersionDisclaimer { 65 | pub fn new(new_version: &str, color: Color) -> Self { 66 | Self { 67 | component: Span::default().foreground(color).spans(&[ 68 | TextSpan::from("termscp "), 69 | TextSpan::new(new_version).underlined().bold(), 70 | TextSpan::from( 71 | " is NOW available! Install update and view release notes with ", 72 | ), 73 | ]), 74 | } 75 | } 76 | } 77 | 78 | impl Component for NewVersionDisclaimer { 79 | fn on(&mut self, _ev: Event) -> Option { 80 | None 81 | } 82 | } 83 | 84 | // -- HelpFooter 85 | 86 | #[derive(MockComponent)] 87 | pub struct HelpFooter { 88 | component: Span, 89 | } 90 | 91 | impl HelpFooter { 92 | pub fn new(key_color: Color) -> Self { 93 | Self { 94 | component: Span::default().spans(&[ 95 | TextSpan::from("").bold().fg(key_color), 96 | TextSpan::from(" Help "), 97 | TextSpan::from("").bold().fg(key_color), 98 | TextSpan::from(" Enter setup "), 99 | TextSpan::from("").bold().fg(key_color), 100 | TextSpan::from(" Change field "), 101 | TextSpan::from("").bold().fg(key_color), 102 | TextSpan::from(" Switch tab "), 103 | TextSpan::from("").bold().fg(key_color), 104 | TextSpan::from(" Switch form "), 105 | TextSpan::from("").bold().fg(key_color), 106 | TextSpan::from(" Submit form "), 107 | TextSpan::from("").bold().fg(key_color), 108 | TextSpan::from(" Quit "), 109 | ]), 110 | } 111 | } 112 | } 113 | 114 | impl Component for HelpFooter { 115 | fn on(&mut self, _ev: Event) -> Option { 116 | None 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/chmod.rs: -------------------------------------------------------------------------------- 1 | use remotefs::fs::UnixPex; 2 | 3 | use super::{FileTransferActivity, LogLevel}; 4 | 5 | impl FileTransferActivity { 6 | pub fn action_local_chmod(&mut self, mode: UnixPex) { 7 | let files = self.get_local_selected_entries().get_files(); 8 | 9 | for file in files { 10 | if let Err(err) = self.host_bridge.chmod(file.path(), mode) { 11 | self.log_and_alert( 12 | LogLevel::Error, 13 | format!( 14 | "could not change mode for {}: {}", 15 | file.path().display(), 16 | err 17 | ), 18 | ); 19 | return; 20 | } 21 | self.log( 22 | LogLevel::Info, 23 | format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), 24 | ); 25 | } 26 | } 27 | 28 | pub fn action_remote_chmod(&mut self, mode: UnixPex) { 29 | let files = self.get_remote_selected_entries().get_files(); 30 | 31 | for file in files { 32 | let mut metadata = file.metadata.clone(); 33 | metadata.mode = Some(mode); 34 | 35 | if let Err(err) = self.client.setstat(file.path(), metadata) { 36 | self.log_and_alert( 37 | LogLevel::Error, 38 | format!( 39 | "could not change mode for {}: {}", 40 | file.path().display(), 41 | err 42 | ), 43 | ); 44 | return; 45 | } 46 | self.log( 47 | LogLevel::Info, 48 | format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), 49 | ); 50 | } 51 | } 52 | 53 | pub fn action_find_local_chmod(&mut self, mode: UnixPex) { 54 | let files = self.get_found_selected_entries().get_files(); 55 | 56 | for file in files { 57 | if let Err(err) = self.host_bridge.chmod(file.path(), mode) { 58 | self.log_and_alert( 59 | LogLevel::Error, 60 | format!( 61 | "could not change mode for {}: {}", 62 | file.path().display(), 63 | err 64 | ), 65 | ); 66 | return; 67 | } 68 | self.log( 69 | LogLevel::Info, 70 | format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), 71 | ); 72 | } 73 | } 74 | 75 | pub fn action_find_remote_chmod(&mut self, mode: UnixPex) { 76 | let files = self.get_found_selected_entries().get_files(); 77 | 78 | for file in files { 79 | let mut metadata = file.metadata.clone(); 80 | metadata.mode = Some(mode); 81 | 82 | if let Err(err) = self.client.setstat(file.path(), metadata) { 83 | self.log_and_alert( 84 | LogLevel::Error, 85 | format!( 86 | "could not change mode for {}: {}", 87 | file.path().display(), 88 | err 89 | ), 90 | ); 91 | return; 92 | } 93 | self.log( 94 | LogLevel::Info, 95 | format!("changed mode to {:#o} for {}", u32::from(mode), file.name()), 96 | ); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/delete.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use remotefs::File; 7 | 8 | use super::{FileTransferActivity, LogLevel, SelectedFile}; 9 | 10 | impl FileTransferActivity { 11 | pub(crate) fn action_local_delete(&mut self) { 12 | match self.get_local_selected_entries() { 13 | SelectedFile::One(entry) => { 14 | // Delete file 15 | self.local_remove_file(&entry); 16 | } 17 | SelectedFile::Many(entries) => { 18 | // Iter files 19 | for (entry, _) in entries.iter() { 20 | // Delete file 21 | self.local_remove_file(entry); 22 | } 23 | 24 | // clear selection 25 | self.host_bridge_mut().clear_queue(); 26 | self.reload_host_bridge_filelist(); 27 | } 28 | SelectedFile::None => {} 29 | } 30 | } 31 | 32 | pub(crate) fn action_remote_delete(&mut self) { 33 | match self.get_remote_selected_entries() { 34 | SelectedFile::One(entry) => { 35 | // Delete file 36 | self.remote_remove_file(&entry); 37 | } 38 | SelectedFile::Many(entries) => { 39 | // Iter files 40 | for (entry, _) in entries.iter() { 41 | // Delete file 42 | self.remote_remove_file(entry); 43 | } 44 | 45 | // clear selection 46 | self.remote_mut().clear_queue(); 47 | self.reload_remote_filelist(); 48 | } 49 | SelectedFile::None => {} 50 | } 51 | } 52 | 53 | pub(crate) fn local_remove_file(&mut self, entry: &File) { 54 | match self.host_bridge.remove(entry) { 55 | Ok(_) => { 56 | // Log 57 | self.log( 58 | LogLevel::Info, 59 | format!("Removed file \"{}\"", entry.path().display()), 60 | ); 61 | } 62 | Err(err) => { 63 | self.log_and_alert( 64 | LogLevel::Error, 65 | format!( 66 | "Could not delete file \"{}\": {}", 67 | entry.path().display(), 68 | err 69 | ), 70 | ); 71 | } 72 | } 73 | } 74 | 75 | pub(crate) fn remote_remove_file(&mut self, entry: &File) { 76 | match self.client.remove_dir_all(entry.path()) { 77 | Ok(_) => { 78 | self.log( 79 | LogLevel::Info, 80 | format!("Removed file \"{}\"", entry.path().display()), 81 | ); 82 | } 83 | Err(err) => { 84 | self.log_and_alert( 85 | LogLevel::Error, 86 | format!( 87 | "Could not delete file \"{}\": {}", 88 | entry.path().display(), 89 | err 90 | ), 91 | ); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/exec.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use super::{FileTransferActivity, LogLevel}; 7 | 8 | impl FileTransferActivity { 9 | pub(crate) fn action_local_exec(&mut self, input: String) { 10 | match self.host_bridge.exec(input.as_str()) { 11 | Ok(output) => { 12 | // Reload files 13 | self.log(LogLevel::Info, format!("\"{input}\": {output}")); 14 | } 15 | Err(err) => { 16 | // Report err 17 | self.log_and_alert( 18 | LogLevel::Error, 19 | format!("Could not execute command \"{input}\": {err}"), 20 | ); 21 | } 22 | } 23 | } 24 | 25 | pub(crate) fn action_remote_exec(&mut self, input: String) { 26 | match self.client.as_mut().exec(input.as_str()) { 27 | Ok((rc, output)) => { 28 | // Reload files 29 | self.log( 30 | LogLevel::Info, 31 | format!("\"{input}\" (exitcode: {rc}): {output}"), 32 | ); 33 | } 34 | Err(err) => { 35 | // Report err 36 | self.log_and_alert( 37 | LogLevel::Error, 38 | format!("Could not execute command \"{input}\": {err}"), 39 | ); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/filter.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use regex::Regex; 4 | use remotefs::File; 5 | use wildmatch::WildMatch; 6 | 7 | use crate::ui::activities::filetransfer::FileTransferActivity; 8 | use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab; 9 | 10 | #[derive(Clone, Debug)] 11 | pub enum Filter { 12 | Regex(Regex), 13 | Wildcard(WildMatch), 14 | } 15 | 16 | impl FromStr for Filter { 17 | type Err = (); 18 | fn from_str(s: &str) -> Result { 19 | // try as regex 20 | if let Ok(regex) = Regex::new(s) { 21 | Ok(Self::Regex(regex)) 22 | } else { 23 | Ok(Self::Wildcard(WildMatch::new(s))) 24 | } 25 | } 26 | } 27 | 28 | impl Filter { 29 | fn matches(&self, s: &str) -> bool { 30 | debug!("matching '{s}' with {:?}", self); 31 | match self { 32 | Self::Regex(re) => re.is_match(s), 33 | Self::Wildcard(wm) => wm.matches(s), 34 | } 35 | } 36 | } 37 | 38 | impl FileTransferActivity { 39 | pub fn filter(&self, filter: &str) -> Vec { 40 | let filter = Filter::from_str(filter).unwrap(); 41 | 42 | match self.browser.tab() { 43 | FileExplorerTab::HostBridge => self.browser.host_bridge().iter_files(), 44 | FileExplorerTab::Remote => self.browser.remote().iter_files(), 45 | _ => return vec![], 46 | } 47 | .filter(|f| filter.matches(&f.name())) 48 | .cloned() 49 | .collect() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/mark.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | use super::FileTransferActivity; 6 | 7 | impl FileTransferActivity { 8 | pub(crate) fn action_mark_file(&mut self, index: usize) { 9 | self.enqueue_file(index); 10 | } 11 | 12 | pub(crate) fn action_mark_all(&mut self) { 13 | self.enqueue_all(); 14 | } 15 | 16 | pub(crate) fn action_mark_clear(&mut self) { 17 | self.clear_queue(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/mkdir.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use std::path::PathBuf; 7 | 8 | use remotefs::fs::UnixPex; 9 | 10 | use super::{FileTransferActivity, LogLevel}; 11 | 12 | impl FileTransferActivity { 13 | pub(crate) fn action_local_mkdir(&mut self, input: String) { 14 | match self 15 | .host_bridge 16 | .mkdir(PathBuf::from(input.as_str()).as_path()) 17 | { 18 | Ok(_) => { 19 | // Reload files 20 | self.log(LogLevel::Info, format!("Created directory \"{input}\"")); 21 | } 22 | Err(err) => { 23 | // Report err 24 | self.log_and_alert( 25 | LogLevel::Error, 26 | format!("Could not create directory \"{input}\": {err}"), 27 | ); 28 | } 29 | } 30 | } 31 | pub(crate) fn action_remote_mkdir(&mut self, input: String) { 32 | match self.client.as_mut().create_dir( 33 | PathBuf::from(input.as_str()).as_path(), 34 | UnixPex::from(0o755), 35 | ) { 36 | Ok(_) => { 37 | // Reload files 38 | self.log(LogLevel::Info, format!("Created directory \"{input}\"")); 39 | } 40 | Err(err) => { 41 | // Report err 42 | self.log_and_alert( 43 | LogLevel::Error, 44 | format!("Could not create directory \"{input}\": {err}"), 45 | ); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/newfile.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use std::fs::File as StdFile; 7 | use std::path::PathBuf; 8 | 9 | use remotefs::fs::Metadata; 10 | 11 | use super::{File, FileTransferActivity, LogLevel}; 12 | 13 | impl FileTransferActivity { 14 | pub(crate) fn action_local_newfile(&mut self, input: String) { 15 | // Check if file exists 16 | let mut file_exists: bool = false; 17 | for file in self.host_bridge().iter_files_all() { 18 | if input == file.name() { 19 | file_exists = true; 20 | } 21 | } 22 | if file_exists { 23 | self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); 24 | return; 25 | } 26 | 27 | // Create file 28 | let file_path: PathBuf = PathBuf::from(input.as_str()); 29 | let writer = match self 30 | .host_bridge 31 | .create_file(file_path.as_path(), &Metadata::default()) 32 | { 33 | Ok(f) => f, 34 | Err(err) => { 35 | self.log_and_alert( 36 | LogLevel::Error, 37 | format!("Could not create file \"{}\": {}", file_path.display(), err), 38 | ); 39 | return; 40 | } 41 | }; 42 | // finalize write 43 | if let Err(err) = self.host_bridge.finalize_write(writer) { 44 | self.log_and_alert( 45 | LogLevel::Error, 46 | format!("Could not write file \"{}\": {}", file_path.display(), err), 47 | ); 48 | return; 49 | } 50 | 51 | self.log( 52 | LogLevel::Info, 53 | format!("Created file \"{}\"", file_path.display()), 54 | ); 55 | } 56 | 57 | pub(crate) fn action_remote_newfile(&mut self, input: String) { 58 | // Check if file exists 59 | let mut file_exists: bool = false; 60 | for file in self.remote().iter_files_all() { 61 | if input == file.name() { 62 | file_exists = true; 63 | } 64 | } 65 | if file_exists { 66 | self.log_and_alert(LogLevel::Warn, format!("File \"{input}\" already exists",)); 67 | return; 68 | } 69 | // Get path on remote 70 | let file_path: PathBuf = PathBuf::from(input.as_str()); 71 | // Create file (on local) 72 | match tempfile::NamedTempFile::new() { 73 | Err(err) => { 74 | self.log_and_alert(LogLevel::Error, format!("Could not create tempfile: {err}")) 75 | } 76 | Ok(tfile) => { 77 | // Stat tempfile 78 | let local_file: File = match self.host_bridge.stat(tfile.path()) { 79 | Err(err) => { 80 | self.log_and_alert( 81 | LogLevel::Error, 82 | format!("Could not stat tempfile: {err}"), 83 | ); 84 | return; 85 | } 86 | Ok(f) => f, 87 | }; 88 | if local_file.is_file() { 89 | // Create file 90 | let reader = Box::new(match StdFile::open(tfile.path()) { 91 | Ok(f) => f, 92 | Err(err) => { 93 | self.log_and_alert( 94 | LogLevel::Error, 95 | format!("Could not open tempfile: {err}"), 96 | ); 97 | return; 98 | } 99 | }); 100 | match self 101 | .client 102 | .create_file(file_path.as_path(), &local_file.metadata, reader) 103 | { 104 | Err(err) => self.log_and_alert( 105 | LogLevel::Error, 106 | format!("Could not create file \"{}\": {}", file_path.display(), err), 107 | ), 108 | Ok(_) => { 109 | self.log( 110 | LogLevel::Info, 111 | format!("Created file \"{}\"", file_path.display()), 112 | ); 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/pending.rs: -------------------------------------------------------------------------------- 1 | //! ## Pending actions 2 | //! 3 | //! this little module exposes the routine to create a pending action on the file transfer activity. 4 | //! A pending action is an action which blocks the execution of the application in await of a certain `Msg`. 5 | 6 | use tuirealm::{PollStrategy, Update}; 7 | 8 | use super::{FileTransferActivity, Msg}; 9 | 10 | impl FileTransferActivity { 11 | /// Block execution of activity, preventing ANY kind of message not specified in the `wait_for` argument. 12 | /// Once `wait_for` clause is satisfied, the function returns. 13 | /// 14 | /// Returns the message which satisfied the clause 15 | /// 16 | /// NOTE: The view is redrawn as usual 17 | pub(super) fn wait_for_pending_msg(&mut self, wait_for: &[Msg]) -> Msg { 18 | self.redraw = true; 19 | loop { 20 | // Poll 21 | match self.app.tick(PollStrategy::Once) { 22 | Ok(mut messages) => { 23 | if !messages.is_empty() { 24 | self.redraw = true; 25 | } 26 | let found = messages.iter().position(|m| wait_for.contains(m)); 27 | // Return if found 28 | if let Some(index) = found { 29 | return messages.remove(index); 30 | } else { 31 | // Update 32 | for msg in messages.into_iter() { 33 | let mut msg = Some(msg); 34 | while msg.is_some() { 35 | msg = self.update(msg); 36 | } 37 | } 38 | } 39 | } 40 | Err(err) => { 41 | error!("Application error: {}", err); 42 | } 43 | } 44 | // Redraw 45 | if self.redraw { 46 | self.view(); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/scan.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use super::{File, FileTransferActivity}; 4 | use crate::ui::activities::filetransfer::lib::browser::FileExplorerTab; 5 | 6 | impl FileTransferActivity { 7 | pub(crate) fn action_scan(&mut self, p: &Path) -> Result, String> { 8 | match self.browser.tab() { 9 | FileExplorerTab::HostBridge | FileExplorerTab::FindHostBridge => self 10 | .host_bridge 11 | .list_dir(p) 12 | .map_err(|e| format!("Failed to list directory: {}", e)), 13 | FileExplorerTab::Remote | FileExplorerTab::FindRemote => self 14 | .client 15 | .list_dir(p) 16 | .map_err(|e| format!("Failed to list directory: {}", e)), 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/submit.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use super::{File, FileTransferActivity}; 7 | 8 | enum SubmitAction { 9 | ChangeDir, 10 | None, 11 | } 12 | 13 | impl FileTransferActivity { 14 | /// Decides which action to perform on submit for local explorer 15 | /// Return true whether the directory changed 16 | pub(crate) fn action_submit_local(&mut self, entry: File) { 17 | let (action, entry) = if entry.is_dir() { 18 | (SubmitAction::ChangeDir, entry) 19 | } else if entry.metadata().symlink.is_some() { 20 | // Stat file 21 | let symlink = entry.metadata().symlink.as_ref().unwrap(); 22 | let stat_file = match self.host_bridge.stat(symlink.as_path()) { 23 | Ok(e) => e, 24 | Err(err) => { 25 | warn!( 26 | "Could not stat file pointed by {} ({}): {}", 27 | entry.path().display(), 28 | symlink.display(), 29 | err 30 | ); 31 | entry 32 | } 33 | }; 34 | (SubmitAction::ChangeDir, stat_file) 35 | } else { 36 | (SubmitAction::None, entry) 37 | }; 38 | if let (SubmitAction::ChangeDir, entry) = (action, entry) { 39 | self.action_enter_local_dir(entry) 40 | } 41 | } 42 | 43 | /// Decides which action to perform on submit for remote explorer 44 | /// Return true whether the directory changed 45 | pub(crate) fn action_submit_remote(&mut self, entry: File) { 46 | let (action, entry) = if entry.is_dir() { 47 | (SubmitAction::ChangeDir, entry) 48 | } else if entry.metadata().symlink.is_some() { 49 | // Stat file 50 | let symlink = entry.metadata().symlink.as_ref().unwrap(); 51 | let stat_file = match self.client.stat(symlink.as_path()) { 52 | Ok(e) => e, 53 | Err(err) => { 54 | warn!( 55 | "Could not stat file pointed by {} ({}): {}", 56 | entry.path().display(), 57 | symlink.display(), 58 | err 59 | ); 60 | entry 61 | } 62 | }; 63 | (SubmitAction::ChangeDir, stat_file) 64 | } else { 65 | (SubmitAction::None, entry) 66 | }; 67 | if let (SubmitAction::ChangeDir, entry) = (action, entry) { 68 | self.action_enter_remote_dir(entry) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/symlink.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use std::path::PathBuf; 7 | 8 | use super::{FileTransferActivity, LogLevel}; 9 | 10 | impl FileTransferActivity { 11 | /// Create symlink on localhost 12 | pub(crate) fn action_local_symlink(&mut self, name: String) { 13 | if let Some(entry) = self.get_local_selected_file() { 14 | match self 15 | .host_bridge 16 | .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) 17 | { 18 | Ok(_) => { 19 | self.log( 20 | LogLevel::Info, 21 | format!( 22 | "Created symlink at {}, pointing to {}", 23 | name, 24 | entry.path().display() 25 | ), 26 | ); 27 | } 28 | Err(err) => { 29 | self.log_and_alert(LogLevel::Error, format!("Could not create symlink: {err}")); 30 | } 31 | } 32 | } 33 | } 34 | 35 | /// Copy file on remote 36 | pub(crate) fn action_remote_symlink(&mut self, name: String) { 37 | if let Some(entry) = self.get_remote_selected_file() { 38 | match self 39 | .client 40 | .symlink(PathBuf::from(name.as_str()).as_path(), entry.path()) 41 | { 42 | Ok(_) => { 43 | self.log( 44 | LogLevel::Info, 45 | format!( 46 | "Created symlink at {}, pointing to {}", 47 | name, 48 | entry.path().display() 49 | ), 50 | ); 51 | } 52 | Err(err) => { 53 | self.log_and_alert( 54 | LogLevel::Error, 55 | format!( 56 | "Could not create symlink pointing to {}: {}", 57 | entry.path().display(), 58 | err 59 | ), 60 | ); 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/actions/walkdir.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | // locals 6 | use std::path::{Path, PathBuf}; 7 | 8 | use super::{File, FileTransferActivity}; 9 | use crate::ui::activities::filetransfer::lib::walkdir::WalkdirStates; 10 | 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | pub enum WalkdirError { 13 | Aborted, 14 | Error(String), 15 | } 16 | 17 | impl FileTransferActivity { 18 | pub(crate) fn action_walkdir_local(&mut self) -> Result, WalkdirError> { 19 | let mut acc = Vec::with_capacity(32_768); 20 | 21 | let pwd = self 22 | .host_bridge 23 | .pwd() 24 | .map_err(|e| WalkdirError::Error(e.to_string()))?; 25 | 26 | self.walkdir(&mut acc, &pwd, |activity, path| { 27 | activity 28 | .host_bridge 29 | .list_dir(path) 30 | .map_err(|e| e.to_string()) 31 | })?; 32 | 33 | Ok(acc) 34 | } 35 | 36 | pub(crate) fn action_walkdir_remote(&mut self) -> Result, WalkdirError> { 37 | let mut acc = Vec::with_capacity(32_768); 38 | 39 | let pwd = self 40 | .client 41 | .pwd() 42 | .map_err(|e| WalkdirError::Error(e.to_string()))?; 43 | 44 | self.walkdir(&mut acc, &pwd, |activity, path| { 45 | activity.client.list_dir(path).map_err(|e| e.to_string()) 46 | })?; 47 | 48 | Ok(acc) 49 | } 50 | 51 | fn walkdir( 52 | &mut self, 53 | acc: &mut Vec, 54 | path: &Path, 55 | list_dir_fn: F, 56 | ) -> Result<(), WalkdirError> 57 | where 58 | F: Fn(&mut Self, &Path) -> Result, String> + Copy, 59 | { 60 | // init acc if empty 61 | if acc.is_empty() { 62 | self.init_walkdir(); 63 | } 64 | 65 | // list current directory 66 | let dir_entries = list_dir_fn(self, path).map_err(WalkdirError::Error)?; 67 | 68 | // get dirs to scan later 69 | let dirs = dir_entries 70 | .iter() 71 | .filter(|entry| entry.is_dir()) 72 | .map(|entry| entry.path.clone()) 73 | .collect::>(); 74 | 75 | // extend acc 76 | acc.extend(dir_entries.clone()); 77 | // update view 78 | self.update_walkdir_entries(acc.len()); 79 | 80 | // check aborted 81 | self.check_aborted()?; 82 | 83 | for dir in dirs { 84 | self.walkdir(acc, &dir, list_dir_fn)?; 85 | } 86 | 87 | Ok(()) 88 | } 89 | 90 | fn check_aborted(&mut self) -> Result<(), WalkdirError> { 91 | // read events 92 | self.tick(); 93 | 94 | // check if the user wants to abort 95 | if self.walkdir.aborted { 96 | return Err(WalkdirError::Aborted); 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | fn init_walkdir(&mut self) { 103 | self.walkdir = WalkdirStates::default(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/components/misc.rs: -------------------------------------------------------------------------------- 1 | //! ## Components 2 | //! 3 | //! file transfer activity components 4 | 5 | use tui_realm_stdlib::Span; 6 | use tuirealm::props::{Color, TextSpan}; 7 | use tuirealm::{Component, Event, MockComponent, NoUserEvent}; 8 | 9 | use super::Msg; 10 | 11 | #[derive(MockComponent)] 12 | pub struct FooterBar { 13 | component: Span, 14 | } 15 | 16 | impl FooterBar { 17 | pub fn new(key_color: Color) -> Self { 18 | Self { 19 | component: Span::default().spans(&[ 20 | TextSpan::from("").bold().fg(key_color), 21 | TextSpan::from(" Help "), 22 | TextSpan::from("").bold().fg(key_color), 23 | TextSpan::from(" Change tab "), 24 | TextSpan::from("").bold().fg(key_color), 25 | TextSpan::from(" Transfer "), 26 | TextSpan::from("").bold().fg(key_color), 27 | TextSpan::from(" Enter dir "), 28 | TextSpan::from("").bold().fg(key_color), 29 | TextSpan::from(" Save as "), 30 | TextSpan::from("").bold().fg(key_color), 31 | TextSpan::from(" View "), 32 | TextSpan::from("").bold().fg(key_color), 33 | TextSpan::from(" Edit "), 34 | TextSpan::from("").bold().fg(key_color), 35 | TextSpan::from(" Copy "), 36 | TextSpan::from("").bold().fg(key_color), 37 | TextSpan::from(" Rename "), 38 | TextSpan::from("").bold().fg(key_color), 39 | TextSpan::from(" Make dir "), 40 | TextSpan::from("").bold().fg(key_color), 41 | TextSpan::from(" Delete "), 42 | TextSpan::from("").bold().fg(key_color), 43 | TextSpan::from(" Quit "), 44 | ]), 45 | } 46 | } 47 | } 48 | 49 | impl Component for FooterBar { 50 | fn on(&mut self, _: Event) -> Option { 51 | None 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Components 2 | //! 3 | //! file transfer activity components 4 | 5 | use tui_realm_stdlib::Phantom; 6 | use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers}; 7 | use tuirealm::{Component, MockComponent, NoUserEvent}; 8 | 9 | use super::{Msg, PendingActionMsg, TransferMsg, UiMsg}; 10 | 11 | // -- export 12 | mod log; 13 | mod misc; 14 | mod popups; 15 | mod selected_files; 16 | mod transfer; 17 | 18 | pub use misc::FooterBar; 19 | pub use popups::{ 20 | ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, ExecPopup, 21 | FatalPopup, FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, 22 | OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup, 23 | ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, 24 | SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList, 25 | WatcherPopup, 26 | }; 27 | pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote}; 28 | 29 | pub use self::log::Log; 30 | pub use self::selected_files::SelectedFilesList; 31 | 32 | #[derive(Default, MockComponent)] 33 | pub struct GlobalListener { 34 | component: Phantom, 35 | } 36 | 37 | impl Component for GlobalListener { 38 | fn on(&mut self, ev: Event) -> Option { 39 | match ev { 40 | Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { 41 | Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) 42 | } 43 | Event::Keyboard(KeyEvent { 44 | code: Key::Char('q') | Key::Function(10), 45 | modifiers: KeyModifiers::NONE, 46 | }) => Some(Msg::Ui(UiMsg::ShowQuitPopup)), 47 | Event::Keyboard(KeyEvent { 48 | code: Key::Char('h') | Key::Function(1), 49 | modifiers: KeyModifiers::NONE, 50 | }) => Some(Msg::Ui(UiMsg::ShowKeybindingsPopup)), 51 | Event::WindowResize(_, _) => Some(Msg::Ui(UiMsg::WindowResized)), 52 | _ => None, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/components/selected_files.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use tui_realm_stdlib::List; 4 | use tuirealm::command::{Cmd, Direction, Position}; 5 | use tuirealm::event::{Key, KeyEvent}; 6 | use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; 7 | use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; 8 | 9 | use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg}; 10 | 11 | #[derive(MockComponent)] 12 | pub struct SelectedFilesList { 13 | component: List, 14 | paths: Vec, 15 | queue: MarkQueue, 16 | } 17 | 18 | impl SelectedFilesList { 19 | pub fn new( 20 | paths: &[(PathBuf, PathBuf)], 21 | queue: MarkQueue, 22 | color: Color, 23 | title: &'static str, 24 | ) -> Self { 25 | let enqueued_paths = paths 26 | .iter() 27 | .map(|(src, _)| src.clone()) 28 | .collect::>(); 29 | 30 | Self { 31 | queue, 32 | paths: enqueued_paths, 33 | component: List::default() 34 | .borders(Borders::default().color(color).modifiers(BorderType::Plain)) 35 | .rewind(true) 36 | .scroll(true) 37 | .step(4) 38 | .highlighted_color(color) 39 | .highlighted_str("➤ ") 40 | .title(title, Alignment::Left) 41 | .rows( 42 | paths 43 | .iter() 44 | .map(|(src, dest)| { 45 | vec![ 46 | TextSpan::from(Self::filename(src)), 47 | TextSpan::from(" -> "), 48 | TextSpan::from(Self::filename(dest)), 49 | ] 50 | }) 51 | .collect(), 52 | ), 53 | } 54 | } 55 | 56 | fn filename(p: &Path) -> String { 57 | p.file_name() 58 | .unwrap_or_default() 59 | .to_string_lossy() 60 | .to_string() 61 | } 62 | } 63 | 64 | impl Component for SelectedFilesList { 65 | fn on(&mut self, ev: Event) -> Option { 66 | match ev { 67 | Event::Keyboard(KeyEvent { 68 | code: Key::Down, .. 69 | }) => { 70 | self.perform(Cmd::Move(Direction::Down)); 71 | Some(Msg::None) 72 | } 73 | Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { 74 | self.perform(Cmd::Move(Direction::Up)); 75 | Some(Msg::None) 76 | } 77 | Event::Keyboard(KeyEvent { 78 | code: Key::PageDown, 79 | .. 80 | }) => { 81 | self.perform(Cmd::Scroll(Direction::Down)); 82 | Some(Msg::None) 83 | } 84 | Event::Keyboard(KeyEvent { 85 | code: Key::PageUp, .. 86 | }) => { 87 | self.perform(Cmd::Scroll(Direction::Up)); 88 | Some(Msg::None) 89 | } 90 | Event::Keyboard(KeyEvent { 91 | code: Key::Home, .. 92 | }) => { 93 | self.perform(Cmd::GoTo(Position::Begin)); 94 | Some(Msg::None) 95 | } 96 | Event::Keyboard(KeyEvent { code: Key::End, .. }) => { 97 | self.perform(Cmd::GoTo(Position::End)); 98 | Some(Msg::None) 99 | } 100 | Event::Keyboard(KeyEvent { 101 | code: Key::Right, .. 102 | }) => Some(Msg::Ui(UiMsg::BottomPanelRight)), 103 | Event::Keyboard(KeyEvent { 104 | code: Key::Left, .. 105 | }) => Some(Msg::Ui(UiMsg::BottomPanelLeft)), 106 | Event::Keyboard(KeyEvent { 107 | code: Key::BackTab | Key::Tab | Key::Char('p'), 108 | .. 109 | }) => Some(Msg::Ui(UiMsg::LogBackTabbed)), 110 | Event::Keyboard(KeyEvent { 111 | code: Key::Enter | Key::Delete, 112 | .. 113 | }) => { 114 | // unmark the selected file 115 | let State::One(StateValue::Usize(idx)) = self.state() else { 116 | return None; 117 | }; 118 | 119 | let path = self.paths.get(idx)?; 120 | 121 | Some(Msg::Ui(UiMsg::MarkRemove(self.queue, path.clone()))) 122 | } 123 | _ => None, 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/fswatcher.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use super::{FileTransferActivity, LogLevel, TransferPayload}; 4 | use crate::system::watcher::FsChange; 5 | 6 | impl FileTransferActivity { 7 | /// poll file watcher 8 | pub(super) fn poll_watcher(&mut self) { 9 | if self.fswatcher.is_none() { 10 | return; 11 | } 12 | let watcher = self.fswatcher.as_mut().unwrap(); 13 | match watcher.poll() { 14 | Ok(None) => {} 15 | Ok(Some(FsChange::Move(mov))) => { 16 | debug!( 17 | "fs watcher reported a `Move` from {} to {}", 18 | mov.source().display(), 19 | mov.destination().display() 20 | ); 21 | self.move_watched_file(mov.source(), mov.destination()); 22 | } 23 | Ok(Some(FsChange::Remove(remove))) => { 24 | debug!( 25 | "fs watcher reported a `Remove` of {}", 26 | remove.path().display() 27 | ); 28 | self.remove_watched_file(remove.path()); 29 | } 30 | Ok(Some(FsChange::Update(update))) => { 31 | debug!( 32 | "fs watcher reported an `Update` from {} to {}", 33 | update.host_bridge().display(), 34 | update.remote().display() 35 | ); 36 | self.upload_watched_file(update.host_bridge(), update.remote()); 37 | } 38 | Err(err) => { 39 | self.log( 40 | LogLevel::Error, 41 | format!("error while polling file watcher: {err}"), 42 | ); 43 | } 44 | } 45 | } 46 | 47 | fn move_watched_file(&mut self, source: &Path, destination: &Path) { 48 | // stat remote file 49 | trace!( 50 | "renaming watched file {} to {}", 51 | source.display(), 52 | destination.display() 53 | ); 54 | // stat fs entry 55 | let origin = match self.client.stat(source) { 56 | Ok(f) => f, 57 | Err(err) => { 58 | self.log( 59 | LogLevel::Error, 60 | format!( 61 | "failed to stat file to rename {}: {}", 62 | source.display(), 63 | err 64 | ), 65 | ); 66 | return; 67 | } 68 | }; 69 | // rename using action 70 | self.remote_rename_file(&origin, destination) 71 | } 72 | 73 | fn remove_watched_file(&mut self, file: &Path) { 74 | match self.client.remove_dir_all(file) { 75 | Ok(()) => { 76 | self.log( 77 | LogLevel::Info, 78 | format!("removed watched file at {}", file.display()), 79 | ); 80 | } 81 | Err(err) => { 82 | self.log( 83 | LogLevel::Error, 84 | format!("failed to remove watched file {}: {}", file.display(), err), 85 | ); 86 | } 87 | } 88 | } 89 | 90 | fn upload_watched_file(&mut self, host: &Path, remote: &Path) { 91 | // stat host file 92 | let entry = match self.host_bridge.stat(host) { 93 | Ok(e) => e, 94 | Err(err) => { 95 | self.log( 96 | LogLevel::Error, 97 | format!( 98 | "failed to sync file {} with remote (stat failed): {}", 99 | remote.display(), 100 | err 101 | ), 102 | ); 103 | return; 104 | } 105 | }; 106 | // send 107 | trace!( 108 | "syncing host file {} with remote {}", 109 | host.display(), 110 | remote.display() 111 | ); 112 | let remote_path = remote.parent().unwrap_or_else(|| Path::new("/")); 113 | match self.filetransfer_send(TransferPayload::Any(entry), remote_path, None) { 114 | Ok(()) => { 115 | self.log( 116 | LogLevel::Info, 117 | format!( 118 | "synched watched file {} with {}", 119 | host.display(), 120 | remote.display() 121 | ), 122 | ); 123 | } 124 | Err(err) => { 125 | self.log( 126 | LogLevel::Error, 127 | format!("failed to sync watched file {}: {}", remote.display(), err), 128 | ); 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/lib/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## FileTransferActivity 2 | //! 3 | //! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall 4 | 5 | pub(crate) mod browser; 6 | pub(crate) mod transfer; 7 | pub(crate) mod walkdir; 8 | -------------------------------------------------------------------------------- /src/ui/activities/filetransfer/lib/walkdir.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Default)] 2 | pub struct WalkdirStates { 3 | pub aborted: bool, 4 | } 5 | -------------------------------------------------------------------------------- /src/ui/activities/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Activities 2 | //! 3 | //! `activities` is the module which provides all the different activities 4 | //! each activity identifies a layout with its own logic in the UI 5 | 6 | // Locals 7 | use super::context::Context; 8 | // Activities 9 | pub mod auth; 10 | pub mod filetransfer; 11 | pub mod setup; 12 | 13 | const CROSSTERM_MAX_POLL: usize = 10; 14 | 15 | // -- Exit reason 16 | 17 | pub enum ExitReason { 18 | Quit, 19 | Connect, 20 | Disconnect, 21 | EnterSetup, 22 | } 23 | 24 | // -- Activity trait 25 | 26 | pub trait Activity { 27 | /// `on_create` is the function which must be called to initialize the activity. 28 | /// `on_create` must initialize all the data structures used by the activity 29 | /// Context is taken from activity manager and will be released only when activity is destroyed 30 | fn on_create(&mut self, context: Context); 31 | 32 | /// `on_draw` is the function which draws the graphical interface. 33 | /// This function must be called at each tick to refresh the interface 34 | fn on_draw(&mut self); 35 | 36 | /// `will_umount` is the method which must be able to report to the activity manager, whether 37 | /// the activity should be terminated or not. 38 | /// If not, the call will return `None`, otherwise return`Some(ExitReason)` 39 | fn will_umount(&self) -> Option<&ExitReason>; 40 | 41 | /// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity. 42 | /// This function must be called once before terminating the activity. 43 | /// This function finally releases the context 44 | fn on_destroy(&mut self) -> Option; 45 | } 46 | -------------------------------------------------------------------------------- /src/ui/activities/setup/components/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Components 2 | //! 3 | //! setup activity components 4 | 5 | use super::{CommonMsg, ConfigMsg, Msg, SshMsg, ThemeMsg, ViewLayout}; 6 | 7 | mod commons; 8 | mod config; 9 | mod ssh; 10 | mod theme; 11 | 12 | pub(super) use commons::{ErrorPopup, Footer, Header, Keybindings, QuitPopup, SavePopup}; 13 | pub(super) use config::{ 14 | CheckUpdates, DefaultProtocol, GroupDirs, HiddenFiles, LocalFileFmt, NotificationsEnabled, 15 | NotificationsThreshold, PromptOnFileReplace, RemoteFileFmt, SshConfig, TextEditor, 16 | }; 17 | pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername}; 18 | pub(super) use theme::*; 19 | use tui_realm_stdlib::Phantom; 20 | use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; 21 | use tuirealm::{Component, MockComponent}; 22 | 23 | // -- global listener 24 | 25 | #[derive(Default, MockComponent)] 26 | pub struct GlobalListener { 27 | component: Phantom, 28 | } 29 | 30 | impl Component for GlobalListener { 31 | fn on(&mut self, ev: Event) -> Option { 32 | match ev { 33 | Event::Keyboard(KeyEvent { 34 | code: Key::Esc | Key::Function(10), 35 | .. 36 | }) => Some(Msg::Common(CommonMsg::ShowQuitPopup)), 37 | Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { 38 | Some(Msg::Common(CommonMsg::ChangeLayout)) 39 | } 40 | Event::Keyboard(KeyEvent { 41 | code: Key::Char('h'), 42 | modifiers: KeyModifiers::CONTROL, 43 | }) => Some(Msg::Common(CommonMsg::ShowKeybindings)), 44 | Event::Keyboard(KeyEvent { 45 | code: Key::Function(1), 46 | modifiers: KeyModifiers::NONE, 47 | }) => Some(Msg::Common(CommonMsg::ShowKeybindings)), 48 | Event::Keyboard(KeyEvent { 49 | code: Key::Char('r'), 50 | modifiers: KeyModifiers::CONTROL, 51 | }) => Some(Msg::Common(CommonMsg::RevertChanges)), 52 | Event::Keyboard(KeyEvent { 53 | code: Key::Char('s'), 54 | modifiers: KeyModifiers::CONTROL, 55 | }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), 56 | Event::Keyboard(KeyEvent { 57 | code: Key::Function(4), 58 | modifiers: KeyModifiers::NONE, 59 | }) => Some(Msg::Common(CommonMsg::ShowSavePopup)), 60 | Event::WindowResize(_, _) => Some(Msg::Common(CommonMsg::WindowResized)), 61 | _ => None, 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ui/context.rs: -------------------------------------------------------------------------------- 1 | //! ## Context 2 | //! 3 | //! `Context` is the module which provides all the functionalities related to the UI data holder, called Context 4 | 5 | // Locals 6 | use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalBridge}; 7 | 8 | use super::store::Store; 9 | use crate::filetransfer::{FileTransferParams, HostBridgeParams}; 10 | use crate::system::bookmarks_client::BookmarksClient; 11 | use crate::system::config_client::ConfigClient; 12 | use crate::system::theme_provider::ThemeProvider; 13 | 14 | /// Context holds data structures shared by the activities 15 | pub struct Context { 16 | host_bridge_params: Option, 17 | remote_params: Option, 18 | bookmarks_client: Option, 19 | config_client: ConfigClient, 20 | pub(crate) store: Store, 21 | pub(crate) terminal: TerminalBridge, 22 | theme_provider: ThemeProvider, 23 | error: Option, 24 | } 25 | 26 | impl Context { 27 | /// Instantiates a new Context 28 | pub fn new( 29 | bookmarks_client: Option, 30 | config_client: ConfigClient, 31 | theme_provider: ThemeProvider, 32 | error: Option, 33 | ) -> Context { 34 | let mut terminal = TerminalBridge::init_crossterm().expect("Could not initialize terminal"); 35 | let _ = terminal.disable_mouse_capture(); 36 | 37 | Context { 38 | bookmarks_client, 39 | config_client, 40 | host_bridge_params: None, 41 | remote_params: None, 42 | store: Store::init(), 43 | terminal, 44 | theme_provider, 45 | error, 46 | } 47 | } 48 | 49 | // -- getters 50 | 51 | pub fn remote_params(&self) -> Option<&FileTransferParams> { 52 | self.remote_params.as_ref() 53 | } 54 | 55 | pub fn host_bridge_params(&self) -> Option<&HostBridgeParams> { 56 | self.host_bridge_params.as_ref() 57 | } 58 | 59 | pub fn bookmarks_client(&self) -> Option<&BookmarksClient> { 60 | self.bookmarks_client.as_ref() 61 | } 62 | 63 | pub fn bookmarks_client_mut(&mut self) -> Option<&mut BookmarksClient> { 64 | self.bookmarks_client.as_mut() 65 | } 66 | 67 | pub fn config(&self) -> &ConfigClient { 68 | &self.config_client 69 | } 70 | 71 | pub fn config_mut(&mut self) -> &mut ConfigClient { 72 | &mut self.config_client 73 | } 74 | 75 | pub(crate) fn store(&self) -> &Store { 76 | &self.store 77 | } 78 | 79 | pub(crate) fn store_mut(&mut self) -> &mut Store { 80 | &mut self.store 81 | } 82 | 83 | pub fn theme_provider(&self) -> &ThemeProvider { 84 | &self.theme_provider 85 | } 86 | 87 | pub fn theme_provider_mut(&mut self) -> &mut ThemeProvider { 88 | &mut self.theme_provider 89 | } 90 | 91 | pub fn terminal(&mut self) -> &mut TerminalBridge { 92 | &mut self.terminal 93 | } 94 | 95 | // -- setter 96 | 97 | pub fn set_remote_params(&mut self, params: FileTransferParams) { 98 | self.remote_params = Some(params); 99 | } 100 | 101 | pub fn set_host_bridge_params(&mut self, params: HostBridgeParams) { 102 | self.host_bridge_params = Some(params); 103 | } 104 | 105 | // -- error 106 | 107 | /// Get error message and remove it from the context 108 | pub fn error(&mut self) -> Option { 109 | self.error.take() 110 | } 111 | 112 | pub fn set_error(&mut self, error: String) { 113 | self.error = Some(error); 114 | } 115 | } 116 | 117 | impl Drop for Context { 118 | fn drop(&mut self) { 119 | if let Err(err) = self.terminal.restore() { 120 | error!("Could not restore terminal: {err}"); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Ui 2 | //! 3 | //! `ui` is the module which provides all the functionalities related to the UI 4 | 5 | // Modules 6 | pub mod activities; 7 | pub mod context; 8 | pub(crate) mod store; 9 | -------------------------------------------------------------------------------- /src/utils/crypto.rs: -------------------------------------------------------------------------------- 1 | //! ## Crypto 2 | //! 3 | //! `crypto` is the module which provides utilities for crypting 4 | 5 | // Ext 6 | use magic_crypt::MagicCryptTrait; 7 | 8 | /// Crypt a string using AES128; output is returned as a BASE64 string 9 | pub fn aes128_b64_crypt(key: &str, input: &str) -> String { 10 | let crypter = new_magic_crypt!(key, 128); 11 | crypter.encrypt_str_to_base64(input) 12 | } 13 | 14 | /// Decrypt a string using AES128 15 | pub fn aes128_b64_decrypt(key: &str, secret: &str) -> Result { 16 | let crypter = new_magic_crypt!(key, 128); 17 | crypter.decrypt_base64_to_string(secret) 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | 23 | use pretty_assertions::assert_eq; 24 | 25 | use super::*; 26 | 27 | #[test] 28 | fn test_utils_crypto_aes128() { 29 | let key: &str = "MYSUPERSECRETKEY"; 30 | let input: &str = "Hello world!"; 31 | let secret: String = aes128_b64_crypt(key, input); 32 | assert_eq!(secret.as_str(), "z4Z6LpcpYqBW4+bkIok+5A=="); 33 | assert_eq!( 34 | aes128_b64_decrypt(key, secret.as_str()) 35 | .ok() 36 | .unwrap() 37 | .as_str(), 38 | input 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/file.rs: -------------------------------------------------------------------------------- 1 | //! ## File 2 | //! 3 | //! `file` is the module which exposes file related utilities 4 | 5 | use std::fs::{File, OpenOptions}; 6 | use std::io; 7 | use std::path::Path; 8 | 9 | /// Open file provided as parameter 10 | pub fn open_file

(filename: P, create: bool, write: bool, append: bool) -> io::Result 11 | where 12 | P: AsRef, 13 | { 14 | OpenOptions::new() 15 | .create(create) 16 | .write(write) 17 | .append(append) 18 | .truncate(!append) 19 | .open(filename) 20 | } 21 | 22 | #[cfg(test)] 23 | mod tests { 24 | use super::*; 25 | 26 | #[test] 27 | fn test_utils_file_open() { 28 | let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); 29 | assert!(open_file(tmpfile.path(), true, true, true).is_ok()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! ## Utils 2 | //! 3 | //! `utils` is the module which provides utilities of different kind 4 | 5 | // modules 6 | pub mod crypto; 7 | pub mod file; 8 | pub mod fmt; 9 | pub mod parser; 10 | pub mod path; 11 | pub mod random; 12 | pub mod ssh; 13 | pub mod string; 14 | pub mod tty; 15 | pub mod ui; 16 | 17 | #[cfg(test)] 18 | #[allow(dead_code)] 19 | pub mod test_helpers; 20 | -------------------------------------------------------------------------------- /src/utils/random.rs: -------------------------------------------------------------------------------- 1 | //! ## Random 2 | //! 3 | //! `random` is the module which provides utilities for rand 4 | 5 | // Ext 6 | 7 | use rand::distr::Alphanumeric; 8 | use rand::{Rng, rng}; 9 | 10 | /// Generate a random alphanumeric string with provided length 11 | pub fn random_alphanumeric_with_len(len: usize) -> String { 12 | let mut rng = rng(); 13 | std::iter::repeat(()) 14 | .map(|()| rng.sample(Alphanumeric)) 15 | .map(char::from) 16 | .take(len) 17 | .collect() 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | 23 | use pretty_assertions::assert_eq; 24 | 25 | use super::*; 26 | 27 | #[test] 28 | fn test_utils_random_alphanumeric_with_len() { 29 | assert_eq!(random_alphanumeric_with_len(256).len(), 256); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/ssh.rs: -------------------------------------------------------------------------------- 1 | use ssh2_config::{ParseRule, SshConfig}; 2 | 3 | pub fn parse_ssh2_config(path: &str) -> Result { 4 | use std::fs::File; 5 | use std::io::BufReader; 6 | 7 | let mut reader = File::open(path) 8 | .map_err(|e| format!("failed to open {path}: {e}")) 9 | .map(BufReader::new)?; 10 | SshConfig::default() 11 | .parse(&mut reader, ParseRule::ALLOW_UNKNOWN_FIELDS) 12 | .map_err(|e| format!("Failed to parse ssh2 config: {e}")) 13 | } 14 | 15 | #[cfg(test)] 16 | mod test { 17 | 18 | use crate::utils::ssh::parse_ssh2_config; 19 | use crate::utils::test_helpers; 20 | 21 | #[test] 22 | fn should_parse_ssh2_config() { 23 | let rsa_key = test_helpers::create_sample_file_with_content( 24 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a", 25 | ); 26 | let ssh_config_file = test_helpers::create_sample_file_with_content(format!( 27 | r#" 28 | Host test 29 | HostName 127.0.0.1 30 | Port 2222 31 | User test 32 | IdentityFile {} 33 | StrictHostKeyChecking no 34 | UserKnownHostsFile /dev/null 35 | "#, 36 | rsa_key.path().display() 37 | )); 38 | 39 | assert!( 40 | parse_ssh2_config( 41 | ssh_config_file 42 | .path() 43 | .to_string_lossy() 44 | .to_string() 45 | .as_str() 46 | ) 47 | .is_ok() 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/string.rs: -------------------------------------------------------------------------------- 1 | //! # String 2 | //! 3 | //! String related utilities 4 | 5 | /// Get a substring considering utf8 characters 6 | pub fn secure_substring(string: &str, start: usize, end: usize) -> String { 7 | assert!(end >= start); 8 | string.chars().take(end).skip(start).collect() 9 | } 10 | 11 | #[cfg(test)] 12 | mod test { 13 | 14 | use super::*; 15 | 16 | #[test] 17 | fn should_get_secure_substring() { 18 | assert_eq!(secure_substring("christian", 2, 5).as_str(), "ris"); 19 | assert_eq!(secure_substring("россия", 3, 5).as_str(), "си"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/test_helpers.rs: -------------------------------------------------------------------------------- 1 | //! ## TestHelpers 2 | //! 3 | //! contains helper functions for tests 4 | 5 | // ext 6 | use std::fs::File as StdFile; 7 | use std::io::Write; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use remotefs::fs::{File, FileType, Metadata}; 11 | use tempfile::NamedTempFile; 12 | 13 | pub fn create_sample_file_entry() -> (File, NamedTempFile) { 14 | // Write 15 | let tmpfile = create_sample_file(); 16 | ( 17 | File { 18 | path: tmpfile.path().to_path_buf(), 19 | metadata: Metadata::default(), 20 | }, 21 | tmpfile, 22 | ) 23 | } 24 | 25 | /// Create sample file with default lorem ipsum content 26 | pub fn create_sample_file() -> NamedTempFile { 27 | create_sample_file_with_content( 28 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus.", 29 | ) 30 | } 31 | 32 | /// Create sample file with provided content 33 | pub fn create_sample_file_with_content(content: impl std::fmt::Display) -> NamedTempFile { 34 | let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); 35 | writeln!(tmpfile, "{content}").unwrap(); 36 | tmpfile 37 | } 38 | 39 | /// Make a file with `name` at specified path 40 | pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result { 41 | let mut p: PathBuf = PathBuf::from(dir); 42 | p.push(filename); 43 | let mut file = StdFile::create(p.as_path())?; 44 | writeln!( 45 | file, 46 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus." 47 | )?; 48 | Ok(p) 49 | } 50 | 51 | /// Make a directory in `dir` 52 | pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> { 53 | let mut p: PathBuf = PathBuf::from(dir); 54 | p.push(dirname); 55 | std::fs::create_dir(p.as_path()) 56 | } 57 | 58 | /// Create a File at specified path 59 | pub fn make_fsentry>(path: P, is_dir: bool) -> File { 60 | let path: PathBuf = path.as_ref().to_path_buf(); 61 | File { 62 | path, 63 | metadata: Metadata::default().file_type(if is_dir { 64 | FileType::Directory 65 | } else { 66 | FileType::File 67 | }), 68 | } 69 | } 70 | 71 | /// Open a file with two handlers, the first is to read, the second is to write 72 | pub fn create_file_ioers(p: &Path) -> (StdFile, StdFile) { 73 | ( 74 | StdFile::open(p).ok().unwrap(), 75 | StdFile::create(p).ok().unwrap(), 76 | ) 77 | } 78 | 79 | mod test { 80 | use pretty_assertions::assert_eq; 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn test_utils_test_helpers_sample_file() { 86 | let _ = create_sample_file_entry(); 87 | } 88 | 89 | #[test] 90 | fn test_utils_test_helpers_make_fsentry() { 91 | assert_eq!( 92 | make_fsentry(PathBuf::from("/tmp/omar.txt"), false) 93 | .name() 94 | .as_str(), 95 | "omar.txt" 96 | ); 97 | assert_eq!( 98 | make_fsentry(PathBuf::from("/tmp/cards"), true) 99 | .name() 100 | .as_str(), 101 | "cards" 102 | ); 103 | } 104 | 105 | #[test] 106 | fn test_utils_test_helpers_make_samples() { 107 | let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); 108 | assert!(make_file_at(tmpdir.path(), "omaroni.txt").is_ok()); 109 | assert!(make_file_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "readme.txt").is_err()); 110 | assert!(make_dir_at(tmpdir.path(), "docs").is_ok()); 111 | assert!(make_dir_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "docs").is_err()); 112 | } 113 | 114 | #[test] 115 | fn test_utils_test_helpers_create_file_ioers() { 116 | let (_, tmp) = create_sample_file_entry(); 117 | let _ = create_file_ioers(tmp.path()); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/utils/tty.rs: -------------------------------------------------------------------------------- 1 | //! ## Utils 2 | //! 3 | //! `Utils` implements utilities functions to work with layouts 4 | 5 | use tuirealm::terminal::{TerminalAdapter, TerminalBridge}; 6 | 7 | /// Read a secret from tty with customisable prompt 8 | pub fn read_secret_from_tty( 9 | terminal_bridge: &mut TerminalBridge, 10 | prompt: impl ToString, 11 | ) -> std::io::Result> 12 | where 13 | T: TerminalAdapter, 14 | { 15 | let _ = terminal_bridge.disable_raw_mode(); 16 | let _ = terminal_bridge.leave_alternate_screen(); 17 | let res = match rpassword::prompt_password(prompt) { 18 | Ok(p) if p.is_empty() => Ok(None), 19 | Ok(p) => Ok(Some(p)), 20 | Err(err) => Err(err), 21 | }; 22 | 23 | let _ = terminal_bridge.enter_alternate_screen(); 24 | let _ = terminal_bridge.enable_raw_mode(); 25 | 26 | res 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/ui.rs: -------------------------------------------------------------------------------- 1 | //! ## Utils 2 | //! 3 | //! `Utils` implements utilities functions to work with layouts 4 | 5 | use tuirealm::ratatui::layout::{Constraint, Direction, Layout, Rect}; 6 | 7 | /// Size type for UI renders 8 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 9 | pub enum Size { 10 | Percentage(u16), 11 | Unit(u16), 12 | } 13 | 14 | /// Ui popup dialog (w x h) 15 | pub struct Popup(pub Size, pub Size); 16 | 17 | impl Popup { 18 | /// Draw popup in provided area 19 | pub fn draw_in(&self, parent: Rect) -> Rect { 20 | let new_area = Layout::default() 21 | .direction(Direction::Vertical) 22 | .constraints(self.height(&parent).as_ref()) 23 | .split(parent); 24 | Layout::default() 25 | .direction(Direction::Horizontal) 26 | .constraints(self.width(&parent).as_ref()) 27 | .split(new_area[1])[1] 28 | } 29 | 30 | fn height(&self, parent: &Rect) -> [Constraint; 3] { 31 | Self::constraints(parent.height, self.1) 32 | } 33 | 34 | fn width(&self, parent: &Rect) -> [Constraint; 3] { 35 | Self::constraints(parent.width, self.0) 36 | } 37 | 38 | fn constraints(area_size: u16, popup_size: Size) -> [Constraint; 3] { 39 | match popup_size { 40 | Size::Percentage(popup_size) => [ 41 | Constraint::Percentage((100 - popup_size) / 2), 42 | Constraint::Percentage(popup_size), 43 | Constraint::Percentage((100 - popup_size) / 2), 44 | ], 45 | Size::Unit(popup_size) => { 46 | let margin = (area_size - popup_size) / 2; 47 | [ 48 | Constraint::Length(margin), 49 | Constraint::Length(popup_size), 50 | Constraint::Length(margin), 51 | ] 52 | } 53 | } 54 | } 55 | } 56 | 57 | #[cfg(test)] 58 | mod tests { 59 | 60 | use pretty_assertions::assert_eq; 61 | 62 | use super::*; 63 | 64 | #[test] 65 | fn test_utils_ui_draw_area_in() { 66 | let area: Rect = Rect::new(0, 0, 1024, 512); 67 | let child: Rect = Popup(Size::Percentage(75), Size::Percentage(30)).draw_in(area); 68 | assert_eq!(child.x, 123); 69 | assert_eq!(child.y, 179); 70 | assert_eq!(child.width, 768); 71 | assert_eq!(child.height, 154); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./site/**/*.{html,js}"], 4 | darkMode: "class", 5 | theme: { 6 | screens: { 7 | sm: { max: "640px" }, 8 | md: "768px", 9 | lg: "1024px", 10 | xl: "1280px", 11 | "2xl": "1536px", 12 | }, 13 | extend: { 14 | colors: { 15 | brand: "#31363b", 16 | }, 17 | fontSize: { 18 | xl: "1.5rem", 19 | "2xl": "2rem", 20 | "3xl": "3.5rem", 21 | "4xl": "7rem", 22 | }, 23 | }, 24 | }, 25 | plugins: [], 26 | }; 27 | -------------------------------------------------------------------------------- /themes/catppuccin-frappe.toml: -------------------------------------------------------------------------------- 1 | auth_address = "#e5c890" 2 | auth_bookmarks = "#e5c890" 3 | auth_password = "#99d1db" 4 | auth_port = "#81c8be" 5 | auth_protocol = "#e5c890" 6 | auth_recents = "#99d1db" 7 | auth_username = "#ca9ee6" 8 | misc_error_dialog = "#e78284" 9 | misc_info_dialog = "#e5c890" 10 | misc_input_dialog = "#c6d0f5" 11 | misc_keys = "#8caaee" 12 | misc_quit_dialog = "#e5c890" 13 | misc_save_dialog = "#81c8be" 14 | misc_warn_dialog = "#ea999c" 15 | transfer_local_explorer_background = "#303446" 16 | transfer_local_explorer_foreground = "#c6d0f5" 17 | transfer_local_explorer_highlighted = "#e5c890" 18 | transfer_log_background = "#303446" 19 | transfer_log_window = "#e5c890" 20 | transfer_progress_bar_full = "##a6d189" 21 | transfer_progress_bar_partial = "##a6d189" 22 | transfer_remote_explorer_background = "#303446" 23 | transfer_remote_explorer_foreground = "#c6d0f5" 24 | transfer_remote_explorer_highlighted = "#99d1db" 25 | transfer_status_hidden = "#99d1db" 26 | transfer_status_sorting = "#e5c890" 27 | transfer_status_sync_browsing = "#e5c890" 28 | -------------------------------------------------------------------------------- /themes/catppuccin-latte.toml: -------------------------------------------------------------------------------- 1 | auth_address = "#fe640b" 2 | auth_bookmarks = "#179299" 3 | auth_password = "#1e66f5" 4 | auth_port = "#04a5e5" 5 | auth_protocol = "#179299" 6 | auth_recents = "#1e66f5" 7 | auth_username = "#ea76cb" 8 | misc_error_dialog = "#d20f39" 9 | misc_info_dialog = "#df8e1d" 10 | misc_input_dialog = "#4c4f69" 11 | misc_keys = "#209fb5" 12 | misc_quit_dialog = "#fe640b" 13 | misc_save_dialog = "#04a5e5" 14 | misc_warn_dialog = "#e64553" 15 | transfer_local_explorer_background = "#eff1f5" 16 | transfer_local_explorer_foreground = "#4c4f69" 17 | transfer_local_explorer_highlighted = "#fe640b" 18 | transfer_log_background = "#eff1f5" 19 | transfer_log_window = "#179299" 20 | transfer_progress_bar_full = "#40a02b" 21 | transfer_progress_bar_partial = "#40a02b" 22 | transfer_remote_explorer_background = "#eff1f5" 23 | transfer_remote_explorer_foreground = "#4c4f69" 24 | transfer_remote_explorer_highlighted = "#1e66f5" 25 | transfer_status_hidden = "#1e66f5" 26 | transfer_status_sorting = "#df8e1d" 27 | transfer_status_sync_browsing = "#179299" 28 | -------------------------------------------------------------------------------- /themes/catppuccin-macchiato.toml: -------------------------------------------------------------------------------- 1 | auth_address = "#f5a97f" 2 | auth_bookmarks = "#a6da95" 3 | auth_password = "#8aadf4" 4 | auth_port = "#8bd5ca" 5 | auth_protocol = "#a6da95" 6 | auth_recents = "#8aadf4" 7 | auth_username = "#f5bde6" 8 | misc_error_dialog = "#ed8796" 9 | misc_info_dialog = "#eed49f" 10 | misc_input_dialog = "#cad3f5" 11 | misc_keys = "#7dc4e4" 12 | misc_quit_dialog = "#f5a97f" 13 | misc_save_dialog = "#8bd5ca" 14 | misc_warn_dialog = "#ee99a0" 15 | transfer_local_explorer_background = "#24273a" 16 | transfer_local_explorer_foreground = "#cad3f5" 17 | transfer_local_explorer_highlighted = "#f5a97f" 18 | transfer_log_background = "#cad3f5" 19 | transfer_log_window = "#a6da95" 20 | transfer_progress_bar_full = "#a6da95" 21 | transfer_progress_bar_partial = "#a6da95" 22 | transfer_remote_explorer_background = "#24273a" 23 | transfer_remote_explorer_foreground = "#cad3f5" 24 | transfer_remote_explorer_highlighted = "#8aadf4" 25 | transfer_status_hidden = "#8aadf4" 26 | transfer_status_sorting = "#eed49f" 27 | transfer_status_sync_browsing = "#a6da95" 28 | -------------------------------------------------------------------------------- /themes/catppuccin-moka.toml: -------------------------------------------------------------------------------- 1 | auth_address = "#fab387" 2 | auth_bookmarks = "#a6e3a1" 3 | auth_password = "#89b4fa" 4 | auth_port = "#89dceb" 5 | auth_protocol = "#a6e3a1" 6 | auth_recents = "#89b4fa" 7 | auth_username = "#f5c2e7" 8 | misc_error_dialog = "#f38ba8" 9 | misc_info_dialog = "#f9e2af" 10 | misc_input_dialog = "#cdd6f4" 11 | misc_keys = "#74c7ec" 12 | misc_quit_dialog = "#fab387" 13 | misc_save_dialog = "#89dceb" 14 | misc_warn_dialog = "#eba0ac" 15 | transfer_local_explorer_background = "#1e1e2e" 16 | transfer_local_explorer_foreground = "#cdd6f4" 17 | transfer_local_explorer_highlighted = "#fab387" 18 | transfer_log_background = "#1e1e2e" 19 | transfer_log_window = "#a6e3a1" 20 | transfer_progress_bar_full = "#a6e3a1" 21 | transfer_progress_bar_partial = "#a6e3a1" 22 | transfer_remote_explorer_background = "#1e1e2e" 23 | transfer_remote_explorer_foreground = "#cdd6f4" 24 | transfer_remote_explorer_highlighted = "#89b4fa" 25 | transfer_status_hidden = "#89b4fa" 26 | transfer_status_sorting = "#f9e2af" 27 | transfer_status_sync_browsing = "#a6e3a1" 28 | -------------------------------------------------------------------------------- /themes/default.toml: -------------------------------------------------------------------------------- 1 | auth_address = "Yellow" 2 | auth_bookmarks = "LightGreen" 3 | auth_password = "LightBlue" 4 | auth_port = "LightCyan" 5 | auth_protocol = "LightGreen" 6 | auth_recents = "LightBlue" 7 | auth_username = "LightMagenta" 8 | misc_error_dialog = "Red" 9 | misc_info_dialog = "LightYellow" 10 | misc_input_dialog = "Default" 11 | misc_keys = "Cyan" 12 | misc_quit_dialog = "Yellow" 13 | misc_save_dialog = "LightCyan" 14 | misc_warn_dialog = "LightRed" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "Yellow" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "LightGreen" 20 | transfer_progress_bar_full = "Green" 21 | transfer_progress_bar_partial = "Green" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "LightBlue" 25 | transfer_status_hidden = "LightBlue" 26 | transfer_status_sorting = "LightYellow" 27 | transfer_status_sync_browsing = "LightGreen" 28 | -------------------------------------------------------------------------------- /themes/earth-wind-fire.toml: -------------------------------------------------------------------------------- 1 | auth_address = "Yellow" 2 | auth_bookmarks = "skyblue" 3 | auth_password = "#c43bff" 4 | auth_port = "lime" 5 | auth_protocol = "orangered" 6 | auth_recents = "deepskyblue" 7 | auth_username = "aqua" 8 | misc_error_dialog = "crimson" 9 | misc_info_dialog = "LightYellow" 10 | misc_input_dialog = "turquoise" 11 | misc_keys = "deeppink" 12 | misc_quit_dialog = "lime" 13 | misc_save_dialog = "gold" 14 | misc_warn_dialog = "orangered" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "aquamarine" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "#c43bff" 20 | transfer_progress_bar_full = "deeppink" 21 | transfer_progress_bar_partial = "turquoise" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "greenyellow" 25 | transfer_status_hidden = "lime" 26 | transfer_status_sorting = "orangered" 27 | transfer_status_sync_browsing = "darkturquoise" 28 | -------------------------------------------------------------------------------- /themes/horizon.toml: -------------------------------------------------------------------------------- 1 | auth_address = "salmon" 2 | auth_bookmarks = "cornflowerblue" 3 | auth_password = "crimson" 4 | auth_port = "tomato" 5 | auth_protocol = "coral" 6 | auth_recents = "royalblue" 7 | auth_username = "orangered" 8 | misc_error_dialog = "crimson" 9 | misc_info_dialog = "coral" 10 | misc_input_dialog = "gold" 11 | misc_keys = "deeppink" 12 | misc_quit_dialog = "coral" 13 | misc_save_dialog = "tomato" 14 | misc_warn_dialog = "orangered" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "lightcoral" 17 | transfer_local_explorer_highlighted = "coral" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "royalblue" 20 | transfer_progress_bar_full = "hotpink" 21 | transfer_progress_bar_partial = "deeppink" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "lightsalmon" 24 | transfer_remote_explorer_highlighted = "salmon" 25 | transfer_status_hidden = "orange" 26 | transfer_status_sorting = "gold" 27 | transfer_status_sync_browsing = "tomato" 28 | -------------------------------------------------------------------------------- /themes/mono-bright.toml: -------------------------------------------------------------------------------- 1 | auth_address = "black" 2 | auth_bookmarks = "#bbbbbb" 3 | auth_password = "black" 4 | auth_port = "black" 5 | auth_protocol = "black" 6 | auth_recents = "#bbbbbb" 7 | auth_username = "black" 8 | misc_error_dialog = "black" 9 | misc_info_dialog = "black" 10 | misc_input_dialog = "black" 11 | misc_keys = "black" 12 | misc_quit_dialog = "black" 13 | misc_save_dialog = "black" 14 | misc_warn_dialog = "black" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "#bbbbbb" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "black" 20 | transfer_progress_bar_full = "black" 21 | transfer_progress_bar_partial = "black" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "#bbbbbb" 25 | transfer_status_hidden = "black" 26 | transfer_status_sorting = "black" 27 | transfer_status_sync_browsing = "black" 28 | -------------------------------------------------------------------------------- /themes/mono-dark.toml: -------------------------------------------------------------------------------- 1 | auth_address = "white" 2 | auth_bookmarks = "white" 3 | auth_password = "white" 4 | auth_port = "white" 5 | auth_protocol = "white" 6 | auth_recents = "white" 7 | auth_username = "white" 8 | misc_error_dialog = "white" 9 | misc_info_dialog = "white" 10 | misc_input_dialog = "white" 11 | misc_keys = "white" 12 | misc_quit_dialog = "white" 13 | misc_save_dialog = "white" 14 | misc_warn_dialog = "white" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "white" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "white" 20 | transfer_progress_bar_full = "white" 21 | transfer_progress_bar_partial = "white" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "white" 25 | transfer_status_hidden = "white" 26 | transfer_status_sorting = "white" 27 | transfer_status_sync_browsing = "white" 28 | -------------------------------------------------------------------------------- /themes/sugarplum.toml: -------------------------------------------------------------------------------- 1 | auth_address = "hotpink" 2 | auth_bookmarks = "pink" 3 | auth_password = "violet" 4 | auth_port = "plum" 5 | auth_protocol = "deeppink" 6 | auth_recents = "lightpink" 7 | auth_username = "orchid" 8 | misc_error_dialog = "mediumvioletred" 9 | misc_info_dialog = "plum" 10 | misc_input_dialog = "plum" 11 | misc_keys = "deeppink" 12 | misc_quit_dialog = "lightcoral" 13 | misc_save_dialog = "violet" 14 | misc_warn_dialog = "hotpink" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "pink" 17 | transfer_local_explorer_highlighted = "hotpink" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "palevioletred" 20 | transfer_progress_bar_full = "hotpink" 21 | transfer_progress_bar_partial = "deeppink" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "plum" 24 | transfer_remote_explorer_highlighted = "violet" 25 | transfer_status_hidden = "violet" 26 | transfer_status_sorting = "plum" 27 | transfer_status_sync_browsing = "orchid" 28 | -------------------------------------------------------------------------------- /themes/ubuntu.toml: -------------------------------------------------------------------------------- 1 | auth_address = "LightYellow" 2 | auth_bookmarks = "springgreen" 3 | auth_password = "deepskyblue" 4 | auth_port = "LightCyan" 5 | auth_protocol = "LightGreen" 6 | auth_recents = "aquamarine" 7 | auth_username = "hotpink" 8 | misc_error_dialog = "orangered" 9 | misc_info_dialog = "LightYellow" 10 | misc_input_dialog = "snow" 11 | misc_keys = "LightCyan" 12 | misc_quit_dialog = "LightYellow" 13 | misc_save_dialog = "LightCyan" 14 | misc_warn_dialog = "tomato" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "Yellow" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "lawngreen" 20 | transfer_progress_bar_full = "lawngreen" 21 | transfer_progress_bar_partial = "lawngreen" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "turquoise" 25 | transfer_status_hidden = "deepskyblue" 26 | transfer_status_sorting = "LightYellow" 27 | transfer_status_sync_browsing = "LightGreen" 28 | -------------------------------------------------------------------------------- /themes/veeso.toml: -------------------------------------------------------------------------------- 1 | auth_address = "Yellow" 2 | auth_bookmarks = "plum" 3 | auth_password = "LightBlue" 4 | auth_port = "turquoise" 5 | auth_protocol = "greenyellow" 6 | auth_recents = "paleturquoise" 7 | auth_username = "deeppink" 8 | misc_error_dialog = "crimson" 9 | misc_info_dialog = "SkyBlue" 10 | misc_input_dialog = "snow" 11 | misc_keys = "deeppink" 12 | misc_quit_dialog = "tomato" 13 | misc_save_dialog = "gold" 14 | misc_warn_dialog = "orangered" 15 | transfer_local_explorer_background = "Default" 16 | transfer_local_explorer_foreground = "Default" 17 | transfer_local_explorer_highlighted = "orange" 18 | transfer_log_background = "Default" 19 | transfer_log_window = "limegreen" 20 | transfer_progress_bar_full = "lawngreen" 21 | transfer_progress_bar_partial = "limegreen" 22 | transfer_remote_explorer_background = "Default" 23 | transfer_remote_explorer_foreground = "Default" 24 | transfer_remote_explorer_highlighted = "turquoise" 25 | transfer_status_hidden = "dodgerblue" 26 | transfer_status_sorting = "LightYellow" 27 | transfer_status_sync_browsing = "palegreen" 28 | --------------------------------------------------------------------------------